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
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
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.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
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 com.github.damontecres.wholphin.util.DebugLogTree
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
|
@ -129,13 +134,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
) {
|
) {
|
||||||
var isRestoringSession by remember { mutableStateOf(true) }
|
var isRestoringSession by remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
if (appPreferences.signInAutomatically) {
|
||||||
serverRepository.restoreSession(
|
try {
|
||||||
appPreferences.currentServerId?.toUUIDOrNull(),
|
serverRepository.restoreSession(
|
||||||
appPreferences.currentUserId?.toUUIDOrNull(),
|
appPreferences.currentServerId?.toUUIDOrNull(),
|
||||||
)
|
appPreferences.currentUserId?.toUUIDOrNull(),
|
||||||
} catch (ex: Exception) {
|
)
|
||||||
Timber.e(ex, "Exception restoring session")
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Exception restoring session")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isRestoringSession = false
|
isRestoringSession = false
|
||||||
}
|
}
|
||||||
|
|
@ -158,12 +165,30 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
key(current) {
|
DisposableEffect(Unit) {
|
||||||
val initialDestination =
|
onDispose {
|
||||||
if (current != null) {
|
if (!appPreferences.signInAutomatically) {
|
||||||
Destination.Home()
|
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 {
|
} 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)
|
val backStack = rememberNavBackStack(initialDestination)
|
||||||
navigationManager.backStack = backStack
|
navigationManager.backStack = backStack
|
||||||
|
|
@ -206,4 +231,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
appUpgradeHandler.run()
|
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(
|
@Database(
|
||||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||||
version = 9,
|
version = 10,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -32,6 +32,7 @@ import java.util.UUID
|
||||||
AutoMigration(6, 7),
|
AutoMigration(6, 7),
|
||||||
AutoMigration(7, 8),
|
AutoMigration(7, 8),
|
||||||
AutoMigration(8, 9),
|
AutoMigration(8, 9),
|
||||||
|
AutoMigration(9, 10),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ interface JellyfinServerDao {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun addUser(user: JellyfinUser): Long
|
fun addUser(user: JellyfinUser): Long
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,10 @@ class ServerRepository
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeSession() {
|
||||||
|
_current.value = null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a successful [AuthenticationResult], switch to the user that just authenticated
|
* 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 {
|
companion object {
|
||||||
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
||||||
context.getSharedPreferences(
|
context.getSharedPreferences(
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,10 @@ data class JellyfinUser(
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val serverId: UUID,
|
val serverId: UUID,
|
||||||
val accessToken: String?,
|
val accessToken: String?,
|
||||||
|
val pin: String? = null,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String =
|
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(
|
data class JellyfinServerUsers(
|
||||||
|
|
|
||||||
|
|
@ -732,6 +732,23 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
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 =
|
val ImageDiskCacheSize =
|
||||||
AppSliderPreference<AppPreferences>(
|
AppSliderPreference<AppPreferences>(
|
||||||
title = R.string.image_cache_size,
|
title = R.string.image_cache_size,
|
||||||
|
|
@ -764,6 +781,7 @@ val basicPreferences =
|
||||||
title = R.string.ui_interface,
|
title = R.string.ui_interface,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
|
AppPreference.SignInAuto,
|
||||||
AppPreference.HomePageItems,
|
AppPreference.HomePageItems,
|
||||||
AppPreference.CombineContinueNext,
|
AppPreference.CombineContinueNext,
|
||||||
AppPreference.RewatchNextUp,
|
AppPreference.RewatchNextUp,
|
||||||
|
|
@ -796,6 +814,7 @@ val basicPreferences =
|
||||||
title = R.string.profile_specific_settings,
|
title = R.string.profile_specific_settings,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
|
AppPreference.RequireProfilePin,
|
||||||
AppPreference.UserPinnedNavDrawerItems,
|
AppPreference.UserPinnedNavDrawerItems,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class AppPreferencesSerializer
|
||||||
autoCheckForUpdates = AppPreference.AutoCheckForUpdates.defaultValue
|
autoCheckForUpdates = AppPreference.AutoCheckForUpdates.defaultValue
|
||||||
sendCrashReports = AppPreference.SendCrashReports.defaultValue
|
sendCrashReports = AppPreference.SendCrashReports.defaultValue
|
||||||
debugLogging = AppPreference.DebugLogging.defaultValue
|
debugLogging = AppPreference.DebugLogging.defaultValue
|
||||||
|
signInAutomatically = AppPreference.SignInAuto.defaultValue
|
||||||
|
|
||||||
playbackPreferences =
|
playbackPreferences =
|
||||||
PlaybackPreferences
|
PlaybackPreferences
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
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.updateAdvancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
||||||
|
|
@ -135,6 +136,7 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) {
|
if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) {
|
||||||
appPreferences.updateData {
|
appPreferences.updateData {
|
||||||
it.updateAdvancedPreferences {
|
it.updateAdvancedPreferences {
|
||||||
|
|
@ -145,6 +147,7 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) {
|
if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) {
|
||||||
appPreferences.updateData {
|
appPreferences.updateData {
|
||||||
it.updateMpvOptions {
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.FocusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
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)
|
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(
|
fun Response<BaseItemDtoQueryResult>.toBaseItems(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
useSeriesForPrimary: Boolean,
|
useSeriesForPrimary: Boolean,
|
||||||
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberBackDropImage(item: BaseItem) {
|
fun rememberBackDropImage(item: BaseItem): String? {
|
||||||
val imageUrlService = LocalImageUrlService.current
|
val imageUrlService = LocalImageUrlService.current
|
||||||
return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) }
|
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.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
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.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||||
|
|
@ -78,6 +79,8 @@ fun PreferencesContent(
|
||||||
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
var preferences by remember { mutableStateOf(initialPreferences) }
|
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())
|
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
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 -> {
|
else -> {
|
||||||
val value = pref.getter.invoke(preferences)
|
val value = pref.getter.invoke(preferences)
|
||||||
ComposablePreference(
|
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.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.isPinned
|
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.NavDrawerPinnedItem
|
||||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
|
@ -47,6 +48,8 @@ class PreferencesViewModel
|
||||||
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
private lateinit var allNavDrawerItems: List<NavDrawerItem>
|
||||||
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
val navDrawerPins = MutableLiveData<Map<NavDrawerItem, Boolean>>(mapOf())
|
||||||
|
|
||||||
|
val currentUser get() = serverRepository.currentUser
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.currentUser.value?.let { user ->
|
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 {
|
companion object {
|
||||||
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
|
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
|
||||||
appPreferences.updateData {
|
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.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
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.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
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.formatBytes
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -195,14 +193,7 @@ fun InstallUpdatePage(
|
||||||
onCancel = {
|
onCancel = {
|
||||||
viewModel.navigationManager.goBack()
|
viewModel.navigationManager.goBack()
|
||||||
},
|
},
|
||||||
modifier =
|
modifier = modifier.dimAndBlur(isDownloading),
|
||||||
modifier
|
|
||||||
.ifElse(
|
|
||||||
isDownloading,
|
|
||||||
Modifier
|
|
||||||
.blur(8.dp)
|
|
||||||
.alpha(.33f),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (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.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
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.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -60,7 +61,7 @@ fun SwitchServerContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier.dimAndBlur(showAddServer),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,11 @@ import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
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.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
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.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -84,10 +86,11 @@ fun SwitchUserContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var switchUserWithPin by remember { mutableStateOf<JellyfinUser?>(null) }
|
||||||
|
|
||||||
currentServer?.let { server ->
|
currentServer?.let { server ->
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier.dimAndBlur(showAddUser || switchUserWithPin != null),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
@ -116,7 +119,11 @@ fun SwitchUserContent(
|
||||||
users = users,
|
users = users,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
onSwitchUser = { user ->
|
onSwitchUser = { user ->
|
||||||
viewModel.switchUser(user)
|
if (user.pin.isNotNullOrBlank()) {
|
||||||
|
switchUserWithPin = user
|
||||||
|
} else {
|
||||||
|
viewModel.switchUser(user)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onAddUser = { showAddUser = true },
|
onAddUser = { showAddUser = true },
|
||||||
onRemoveUser = { user ->
|
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 send_crash_reports = 8;
|
||||||
bool debug_logging = 9;
|
bool debug_logging = 9;
|
||||||
AdvancedPreferences advanced_preferences = 10;
|
AdvancedPreferences advanced_preferences = 10;
|
||||||
|
bool sign_in_automatically = 11;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
<string name="fa_pause" translatable="false"></string>
|
<string name="fa_pause" translatable="false"></string>
|
||||||
<string name="fa_arrow_down_long" 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_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_angle_up" translatable="false"></string>
|
||||||
<string name="fa_angles_up" translatable="false"></string>
|
<string name="fa_angles_up" translatable="false"></string>
|
||||||
<string name="fa_angle_down" translatable="false"></string>
|
<string name="fa_angle_down" translatable="false"></string>
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,15 @@
|
||||||
<string name="discard_change">Discard changes?</string>
|
<string name="discard_change">Discard changes?</string>
|
||||||
<string name="subtitle_margin">Margin</string>
|
<string name="subtitle_margin">Margin</string>
|
||||||
<string name="verbose_logging">Verbose logging</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="image_cache_size">Image disk cache size (MB)</string>
|
||||||
<string name="view_options">View options</string>
|
<string name="view_options">View options</string>
|
||||||
<string name="columns">Columns</string>
|
<string name="columns">Columns</string>
|
||||||
|
|
@ -339,6 +348,7 @@
|
||||||
<string name="show_details">Show details</string>
|
<string name="show_details">Show details</string>
|
||||||
<string name="image_type">Image type</string>
|
<string name="image_type">Image type</string>
|
||||||
|
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
<item>Lowest</item>
|
<item>Lowest</item>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue