mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add setting to protect a user profile with a PIN (#356)
Allows for setting a per-user profile PIN that must be entered to switch to the user. The PIN is a sequence of directional buttons, ie D-Pad left/right/up/down. This PIN is per device per user per server. The PIN is stored locally and not sent nor shared with the server. If you forget your PIN, can still access the app using server credentials which also removes the PIN. Additionally, there is another setting to disable automatically sign in. When enabled (default) the last user is automatically restored, even if there is a PIN. If disabled, a server & user must always be selected when opening the app. Closes #268 Closes #321
This commit is contained in:
parent
a294661520
commit
8ea84b3efe
20 changed files with 845 additions and 30 deletions
|
|
@ -0,0 +1,325 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "addc8fcbddd84a88e20fbda34849cf26",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "servers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pin",
|
||||
"columnName": "pin",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_id_serverId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_serverId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "servers",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serverId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ItemPlayback",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "sourceId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioIndex",
|
||||
"columnName": "audioIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitleIndex",
|
||||
"columnName": "subtitleIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ItemPlayback_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "NavDrawerPinnedItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LibraryDisplayInfo",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sort",
|
||||
"columnName": "sort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "direction",
|
||||
"columnName": "direction",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'{}'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewOptions",
|
||||
"columnName": "viewOptions",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'addc8fcbddd84a88e20fbda34849cf26')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -42,6 +44,7 @@ import com.github.damontecres.wholphin.services.UpdateChecker
|
|||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -49,6 +52,8 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
|||
import com.github.damontecres.wholphin.util.DebugLogTree
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
|
|
@ -129,6 +134,7 @@ class MainActivity : AppCompatActivity() {
|
|||
) {
|
||||
var isRestoringSession by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
if (appPreferences.signInAutomatically) {
|
||||
try {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||
|
|
@ -137,6 +143,7 @@ class MainActivity : AppCompatActivity() {
|
|||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception restoring session")
|
||||
}
|
||||
}
|
||||
isRestoringSession = false
|
||||
}
|
||||
val current by serverRepository.current.observeAsState()
|
||||
|
|
@ -158,12 +165,30 @@ class MainActivity : AppCompatActivity() {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
key(current) {
|
||||
val initialDestination =
|
||||
if (current != null) {
|
||||
Destination.Home()
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!appPreferences.signInAutomatically) {
|
||||
serverRepository.closeSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
key(current?.server?.id, current?.user?.id) {
|
||||
LaunchedEffect(current?.user?.pin) {
|
||||
if (current?.user?.pin?.isNotNullOrBlank() == true) {
|
||||
// If user has a pin, then obscure the window in previews
|
||||
window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
)
|
||||
} else {
|
||||
Destination.ServerList
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
val initialDestination =
|
||||
when {
|
||||
current != null -> Destination.Home()
|
||||
!appPreferences.signInAutomatically -> Destination.ServerList // TODO user list?
|
||||
else -> Destination.ServerList
|
||||
}
|
||||
val backStack = rememberNavBackStack(initialDestination)
|
||||
navigationManager.backStack = backStack
|
||||
|
|
@ -206,4 +231,14 @@ class MainActivity : AppCompatActivity() {
|
|||
appUpgradeHandler.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
val signInAutomatically =
|
||||
runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
|
||||
Timber.i("onRestart: signInAutomatically=$signInAutomatically")
|
||||
if (!signInAutomatically) {
|
||||
serverRepository.closeSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import java.util.UUID
|
|||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||
version = 9,
|
||||
version = 10,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
|
|
@ -32,6 +32,7 @@ import java.util.UUID
|
|||
AutoMigration(6, 7),
|
||||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
AutoMigration(9, 10),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ interface JellyfinServerDao {
|
|||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun addUser(user: JellyfinUser): Long
|
||||
|
||||
@Update
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ class ServerRepository
|
|||
return false
|
||||
}
|
||||
|
||||
fun closeSession() {
|
||||
_current.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a successful [AuthenticationResult], switch to the user that just authenticated
|
||||
*/
|
||||
|
|
@ -225,6 +229,21 @@ class ServerRepository
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun setUserPin(
|
||||
user: JellyfinUser,
|
||||
pin: String?,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val newUser = user.copy(pin = pin)
|
||||
val updatedUser = serverDao.addOrUpdateUser(newUser)
|
||||
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
|
||||
// Updating current user, so push out the change
|
||||
current.value?.let {
|
||||
val newCurrent = it.copy(user = updatedUser)
|
||||
_current.setValueOnMain(newCurrent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences(
|
||||
|
|
|
|||
|
|
@ -50,9 +50,10 @@ data class JellyfinUser(
|
|||
@ColumnInfo(index = true)
|
||||
val serverId: UUID,
|
||||
val accessToken: String?,
|
||||
val pin: String? = null,
|
||||
) {
|
||||
override fun toString(): String =
|
||||
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken=${accessToken.isNotNullOrBlank()})"
|
||||
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})"
|
||||
}
|
||||
|
||||
data class JellyfinServerUsers(
|
||||
|
|
|
|||
|
|
@ -732,6 +732,23 @@ sealed interface AppPreference<Pref, T> {
|
|||
summaryOff = R.string.disabled,
|
||||
)
|
||||
|
||||
val SignInAuto =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.sign_in_auto,
|
||||
defaultValue = true,
|
||||
getter = { it.signInAutomatically },
|
||||
setter = { prefs, value ->
|
||||
prefs.update { signInAutomatically = value }
|
||||
},
|
||||
summaryOn = R.string.enabled,
|
||||
summaryOff = R.string.disabled,
|
||||
)
|
||||
|
||||
val RequireProfilePin =
|
||||
AppClickablePreference<AppPreferences>(
|
||||
title = R.string.require_pin_code,
|
||||
)
|
||||
|
||||
val ImageDiskCacheSize =
|
||||
AppSliderPreference<AppPreferences>(
|
||||
title = R.string.image_cache_size,
|
||||
|
|
@ -764,6 +781,7 @@ val basicPreferences =
|
|||
title = R.string.ui_interface,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.SignInAuto,
|
||||
AppPreference.HomePageItems,
|
||||
AppPreference.CombineContinueNext,
|
||||
AppPreference.RewatchNextUp,
|
||||
|
|
@ -796,6 +814,7 @@ val basicPreferences =
|
|||
title = R.string.profile_specific_settings,
|
||||
preferences =
|
||||
listOf(
|
||||
AppPreference.RequireProfilePin,
|
||||
AppPreference.UserPinnedNavDrawerItems,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class AppPreferencesSerializer
|
|||
autoCheckForUpdates = AppPreference.AutoCheckForUpdates.defaultValue
|
||||
sendCrashReports = AppPreference.SendCrashReports.defaultValue
|
||||
debugLogging = AppPreference.DebugLogging.defaultValue
|
||||
signInAutomatically = AppPreference.SignInAuto.defaultValue
|
||||
|
||||
playbackPreferences =
|
||||
PlaybackPreferences
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
|
|||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.update
|
||||
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
||||
|
|
@ -135,6 +136,7 @@ suspend fun upgradeApp(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) {
|
||||
appPreferences.updateData {
|
||||
it.updateAdvancedPreferences {
|
||||
|
|
@ -145,6 +147,7 @@ suspend fun upgradeApp(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) {
|
||||
appPreferences.updateData {
|
||||
it.updateMpvOptions {
|
||||
|
|
@ -152,4 +155,12 @@ suspend fun upgradeApp(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4-4-g0"))) {
|
||||
appPreferences.updateData {
|
||||
it.update {
|
||||
signInAutomatically = AppPreference.SignInAuto.defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
|
|
@ -398,13 +400,21 @@ fun logTab(
|
|||
|
||||
suspend fun <T> onMain(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
fun Modifier.dimAndBlur(enabled: Boolean) =
|
||||
this.ifElse(
|
||||
enabled,
|
||||
Modifier
|
||||
.alpha(.5f)
|
||||
.blur(16.dp),
|
||||
)
|
||||
|
||||
fun Response<BaseItemDtoQueryResult>.toBaseItems(
|
||||
api: ApiClient,
|
||||
useSeriesForPrimary: Boolean,
|
||||
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
|
||||
@Composable
|
||||
fun rememberBackDropImage(item: BaseItem) {
|
||||
fun rememberBackDropImage(item: BaseItem): String? {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.preferences.uiPreferences
|
|||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||
|
|
@ -78,6 +79,8 @@ fun PreferencesContent(
|
|||
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||
val state = rememberLazyListState()
|
||||
var preferences by remember { mutableStateOf(initialPreferences) }
|
||||
val currentUser by viewModel.currentUser.observeAsState()
|
||||
var showPinFlow by remember { mutableStateOf(false) }
|
||||
|
||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||
|
|
@ -365,6 +368,19 @@ fun PreferencesContent(
|
|||
)
|
||||
}
|
||||
|
||||
AppPreference.RequireProfilePin -> {
|
||||
SwitchPreference(
|
||||
title = stringResource(pref.title),
|
||||
value = currentUser?.pin.isNotNullOrBlank(),
|
||||
onClick = {
|
||||
showPinFlow = true
|
||||
},
|
||||
summaryOn = stringResource(R.string.enabled),
|
||||
summaryOff = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
|
|
@ -408,6 +424,22 @@ fun PreferencesContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (showPinFlow && currentUser != null) {
|
||||
currentUser?.let { user ->
|
||||
SetPinFlow(
|
||||
currentPin = user.pin,
|
||||
onAddPin = {
|
||||
viewModel.setPin(user, it)
|
||||
showPinFlow = false
|
||||
},
|
||||
onRemovePin = {
|
||||
viewModel.setPin(user, null)
|
||||
showPinFlow = false
|
||||
},
|
||||
onDismissRequest = { showPinFlow = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
|||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.isPinned
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
|
|
@ -47,6 +48,8 @@ class PreferencesViewModel
|
|||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
||||
|
||||
val currentUser get() = serverRepository.currentUser
|
||||
|
||||
init {
|
||||
viewModelScope.launchIO {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
|
|
@ -100,6 +103,15 @@ class PreferencesViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun setPin(
|
||||
user: JellyfinUser,
|
||||
pin: String?,
|
||||
) {
|
||||
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||
serverRepository.setUserPin(user, pin)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
|
||||
appPreferences.updateData {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package com.github.damontecres.wholphin.ui.preferences
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.setup.PinEntryCreate
|
||||
|
||||
enum class PinFlowState {
|
||||
ENTER_PIN_TO_REMOVE,
|
||||
ENTER_PIN,
|
||||
CONFIRM_PIN,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetPinFlow(
|
||||
currentPin: String?,
|
||||
onAddPin: (String) -> Unit,
|
||||
onRemovePin: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var flowState by
|
||||
remember {
|
||||
mutableStateOf(if (currentPin.isNullOrBlank()) PinFlowState.ENTER_PIN else PinFlowState.ENTER_PIN_TO_REMOVE)
|
||||
}
|
||||
var pendingPin by remember { mutableStateOf("") }
|
||||
|
||||
AnimatedContent(flowState) { state ->
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
when (state) {
|
||||
PinFlowState.ENTER_PIN_TO_REMOVE -> {
|
||||
PinEntryCreate(
|
||||
title = R.string.enter_pin,
|
||||
onTextChange = {
|
||||
if (it == currentPin) onRemovePin.invoke()
|
||||
},
|
||||
onConfirm = null,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
PinFlowState.ENTER_PIN -> {
|
||||
PinEntryCreate(
|
||||
title = R.string.enter_pin,
|
||||
onTextChange = {},
|
||||
onConfirm = {
|
||||
if (it.length >= 4) {
|
||||
pendingPin = it
|
||||
flowState = PinFlowState.CONFIRM_PIN
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.pin_too_short),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
PinFlowState.CONFIRM_PIN -> {
|
||||
PinEntryCreate(
|
||||
title = R.string.confirm_pin,
|
||||
onTextChange = {},
|
||||
onConfirm = {
|
||||
if (it == pendingPin) {
|
||||
onAddPin.invoke(pendingPin)
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.incorrect),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
flowState = PinFlowState.ENTER_PIN
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
|
|
@ -58,8 +56,8 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
|||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.dimAndBlur
|
||||
import com.github.damontecres.wholphin.ui.formatBytes
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -195,14 +193,7 @@ fun InstallUpdatePage(
|
|||
onCancel = {
|
||||
viewModel.navigationManager.goBack()
|
||||
},
|
||||
modifier =
|
||||
modifier
|
||||
.ifElse(
|
||||
isDownloading,
|
||||
Modifier
|
||||
.blur(8.dp)
|
||||
.alpha(.33f),
|
||||
),
|
||||
modifier = modifier.dimAndBlur(isDownloading),
|
||||
)
|
||||
}
|
||||
if (isDownloading) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
package com.github.damontecres.wholphin.ui.setup
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.playback.isEnterKey
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
||||
@Composable
|
||||
fun PinEntry(
|
||||
onTextChange: (String) -> Unit,
|
||||
onClickServerAuth: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var input by remember { mutableStateOf("") }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.enter_pin),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
PinArrowRow(Modifier.align(Alignment.CenterHorizontally))
|
||||
PinEntryDots(input.length, Modifier.align(Alignment.CenterHorizontally))
|
||||
|
||||
Button(
|
||||
onClick = onClickServerAuth,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.onKeyEvent {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
var str = input
|
||||
str +=
|
||||
when (it.key) {
|
||||
Key.DirectionUp -> "U"
|
||||
Key.DirectionRight -> "R"
|
||||
Key.DirectionDown -> "D"
|
||||
Key.DirectionLeft -> "L"
|
||||
else -> return@onKeyEvent false
|
||||
}
|
||||
onTextChange.invoke(str)
|
||||
input = str
|
||||
return@onKeyEvent true
|
||||
} else {
|
||||
return@onKeyEvent false
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.use_server_credentials),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.will_remove_pin),
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PinEntryCreate(
|
||||
@StringRes title: Int,
|
||||
onTextChange: (String) -> Unit,
|
||||
onConfirm: ((String) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var input by remember { mutableStateOf("") }
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.onKeyEvent {
|
||||
if (isEnterKey(it)) {
|
||||
onConfirm?.invoke(input)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
var str = input
|
||||
str +=
|
||||
when (it.key) {
|
||||
Key.DirectionUp -> "U"
|
||||
Key.DirectionRight -> "R"
|
||||
Key.DirectionDown -> "D"
|
||||
Key.DirectionLeft -> "L"
|
||||
else -> return@onKeyEvent false
|
||||
}
|
||||
onTextChange.invoke(str)
|
||||
input = str
|
||||
return@onKeyEvent true
|
||||
} else {
|
||||
return@onKeyEvent false
|
||||
}
|
||||
}.focusable(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
PinArrowRow(Modifier.align(Alignment.CenterHorizontally))
|
||||
PinEntryDots(input.length, Modifier.align(Alignment.CenterHorizontally))
|
||||
if (onConfirm != null) {
|
||||
Text(
|
||||
text = stringResource(R.string.press_enter_to_confirm),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PinArrowRow(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
val arrows =
|
||||
listOf(R.string.fa_arrow_left_long, R.string.fa_arrow_up_long, R.string.fa_arrow_right_long, R.string.fa_arrow_down_long)
|
||||
arrows.forEach {
|
||||
Text(
|
||||
text = stringResource(it),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontFamily = FontAwesome,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PinEntryDots(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.defaultMinSize(minWidth = 180.dp, minHeight = 40.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
|
||||
shape = CircleShape,
|
||||
).padding(vertical = 16.dp),
|
||||
) {
|
||||
repeat(count) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f))
|
||||
.size(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PinEntryDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onTextChange: (String) -> Unit,
|
||||
onClickServerAuth: () -> Unit,
|
||||
) {
|
||||
BasicDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
PinEntry(
|
||||
onTextChange = onTextChange,
|
||||
onClickServerAuth = onClickServerAuth,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
private fun PinEntryPreview() {
|
||||
WholphinTheme {
|
||||
PinEntry(
|
||||
onTextChange = {},
|
||||
onClickServerAuth = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import com.github.damontecres.wholphin.R
|
|||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||
import com.github.damontecres.wholphin.ui.dimAndBlur
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
|
@ -60,7 +61,7 @@ fun SwitchServerContent(
|
|||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
modifier = modifier.dimAndBlur(showAddServer),
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -40,9 +40,11 @@ import androidx.tv.material3.Text
|
|||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||
import com.github.damontecres.wholphin.ui.dimAndBlur
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -84,10 +86,11 @@ fun SwitchUserContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
var switchUserWithPin by remember { mutableStateOf<JellyfinUser?>(null) }
|
||||
|
||||
currentServer?.let { server ->
|
||||
Box(
|
||||
modifier = modifier,
|
||||
modifier = modifier.dimAndBlur(showAddUser || switchUserWithPin != null),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
@ -116,7 +119,11 @@ fun SwitchUserContent(
|
|||
users = users,
|
||||
currentUser = currentUser,
|
||||
onSwitchUser = { user ->
|
||||
if (user.pin.isNotNullOrBlank()) {
|
||||
switchUserWithPin = user
|
||||
} else {
|
||||
viewModel.switchUser(user)
|
||||
}
|
||||
},
|
||||
onAddUser = { showAddUser = true },
|
||||
onRemoveUser = { user ->
|
||||
|
|
@ -300,6 +307,18 @@ fun SwitchUserContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
switchUserWithPin?.let { user ->
|
||||
PinEntryDialog(
|
||||
onDismissRequest = { switchUserWithPin = null },
|
||||
onClickServerAuth = {
|
||||
showAddUser = true
|
||||
switchUserWithPin = null
|
||||
},
|
||||
onTextChange = {
|
||||
if (it == user.pin) viewModel.switchUser(user)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,4 +151,5 @@ message AppPreferences {
|
|||
bool send_crash_reports = 8;
|
||||
bool debug_logging = 9;
|
||||
AdvancedPreferences advanced_preferences = 10;
|
||||
bool sign_in_automatically = 11;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
<string name="fa_pause" translatable="false"></string>
|
||||
<string name="fa_arrow_down_long" translatable="false"></string>
|
||||
<string name="fa_arrow_up_long" translatable="false"></string>
|
||||
<string name="fa_arrow_left_long" translatable="false"></string>
|
||||
<string name="fa_arrow_right_long" translatable="false"></string>
|
||||
<string name="fa_angle_up" translatable="false"></string>
|
||||
<string name="fa_angles_up" translatable="false"></string>
|
||||
<string name="fa_angle_down" translatable="false"></string>
|
||||
|
|
|
|||
|
|
@ -331,6 +331,15 @@
|
|||
<string name="discard_change">Discard changes?</string>
|
||||
<string name="subtitle_margin">Margin</string>
|
||||
<string name="verbose_logging">Verbose logging</string>
|
||||
<string name="enter_pin">Enter PIN</string>
|
||||
<string name="sign_in_auto">Sign in automatically</string>
|
||||
<string name="use_server_credentials">Login via server</string>
|
||||
<string name="press_enter_to_confirm">Press center to confirm</string>
|
||||
<string name="require_pin_code">Require PIN for profile</string>
|
||||
<string name="confirm_pin">Confirm PIN</string>
|
||||
<string name="incorrect">Incorrect</string>
|
||||
<string name="pin_too_short">PIN must be 4 keys or longer</string>
|
||||
<string name="will_remove_pin">Will remove PIN</string>
|
||||
<string name="image_cache_size">Image disk cache size (MB)</string>
|
||||
<string name="view_options">View options</string>
|
||||
<string name="columns">Columns</string>
|
||||
|
|
@ -339,6 +348,7 @@
|
|||
<string name="show_details">Show details</string>
|
||||
<string name="image_type">Image type</string>
|
||||
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
<item>Lowest</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue