mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add option to require server login for user profile (#1367)
## Description This PR adds a new method of protecting a user profile when switching to it: server login. When enabled, switching to the user requires authenticating with the server again via Quick Connect or username & password. This is basically the same as the official client. ### Related issues Closes #321 Related to #1330 - might cause merge conflicts Related to #1287 - definitely causes merge conflicts ### Testing Emulator with various uses cases including auto sign-in, switching between server login & PIN protection, and removing the protection ## Screenshots N/A, the login looks identical as adding a new user ## AI or LLM usage None
This commit is contained in:
parent
1463480e26
commit
45d466d76f
12 changed files with 813 additions and 35 deletions
|
|
@ -0,0 +1,659 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 32,
|
||||||
|
"identityHash": "934c38e7f4eb68c9a223ddb5c7e3e35d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "lastUsed",
|
||||||
|
"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, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` 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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "requireLogin",
|
||||||
|
"columnName": "requireLogin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "lastUsed",
|
||||||
|
"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, `order` INTEGER NOT NULL DEFAULT -1, 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playback_effects",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "jellyfinUserRowId",
|
||||||
|
"columnName": "jellyfinUserRowId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.rotation",
|
||||||
|
"columnName": "rotation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.brightness",
|
||||||
|
"columnName": "brightness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.contrast",
|
||||||
|
"columnName": "contrast",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.saturation",
|
||||||
|
"columnName": "saturation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.hue",
|
||||||
|
"columnName": "hue",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.red",
|
||||||
|
"columnName": "red",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.green",
|
||||||
|
"columnName": "green",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.blue",
|
||||||
|
"columnName": "blue",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoFilter.blur",
|
||||||
|
"columnName": "blur",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"jellyfinUserRowId",
|
||||||
|
"itemId",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "PlaybackLanguageChoice",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "audioLanguage",
|
||||||
|
"columnName": "audioLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitleLanguage",
|
||||||
|
"columnName": "subtitleLanguage",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subtitlesDisabled",
|
||||||
|
"columnName": "subtitlesDisabled",
|
||||||
|
"affinity": "INTEGER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"seriesId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ItemTrackModification",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), 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": "trackIndex",
|
||||||
|
"columnName": "trackIndex",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "delayMs",
|
||||||
|
"columnName": "delayMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId",
|
||||||
|
"trackIndex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"rowId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seerr_servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_seerr_servers_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seerr_users",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "jellyfinUserRowId",
|
||||||
|
"columnName": "jellyfinUserRowId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authMethod",
|
||||||
|
"columnName": "authMethod",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "credential",
|
||||||
|
"columnName": "credential",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"jellyfinUserRowId",
|
||||||
|
"serverId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "seerr_servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"jellyfinUserRowId"
|
||||||
|
],
|
||||||
|
"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, '934c38e7f4eb68c9a223ddb5c7e3e35d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -434,8 +434,11 @@ class MainActivityViewModel
|
||||||
appUpgradeHandler.copySubfont(false)
|
appUpgradeHandler.copySubfont(false)
|
||||||
val prefs =
|
val prefs =
|
||||||
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||||
val userHasPin = serverRepository.currentUser.value?.hasPin == true
|
val allowAutoSignIn =
|
||||||
if (prefs.signInAutomatically && !userHasPin) {
|
serverRepository.currentUser.value?.let {
|
||||||
|
!it.hasPin && !it.requireLogin
|
||||||
|
} == true
|
||||||
|
if (prefs.signInAutomatically && allowAutoSignIn) {
|
||||||
val current =
|
val current =
|
||||||
serverRepository.restoreSession(
|
serverRepository.restoreSession(
|
||||||
prefs.currentServerId?.toUUIDOrNull(),
|
prefs.currentServerId?.toUUIDOrNull(),
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,19 @@ import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
import com.github.damontecres.wholphin.data.model.SeerrServer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
import com.github.damontecres.wholphin.data.model.SeerrUser
|
||||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
|
|
@ -40,7 +48,7 @@ import java.util.UUID
|
||||||
SeerrUser::class,
|
SeerrUser::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
version = 31,
|
version = 32,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -55,6 +63,7 @@ import java.util.UUID
|
||||||
AutoMigration(12, 20),
|
AutoMigration(12, 20),
|
||||||
AutoMigration(20, 30),
|
AutoMigration(20, 30),
|
||||||
AutoMigration(30, 31),
|
AutoMigration(30, 31),
|
||||||
|
AutoMigration(31, 32),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
@ -116,6 +125,27 @@ class Converters {
|
||||||
Timber.e(ex, "Error parsing view options")
|
Timber.e(ex, "Error parsing view options")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertToString(dateTime: ZonedDateTime): String = DateTimeFormatter.ISO_ZONED_DATE_TIME.format(dateTime)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun convertToLocalDateTime(dateTime: String): ZonedDateTime = ZonedDateTime.parse(dateTime, DateTimeFormatter.ISO_ZONED_DATE_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor(ZonedDateTime::class.qualifiedName!!, PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): ZonedDateTime =
|
||||||
|
ZonedDateTime.parse(decoder.decodeString(), DateTimeFormatter.ISO_ZONED_DATE_TIME)
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
encoder: Encoder,
|
||||||
|
value: ZonedDateTime,
|
||||||
|
) {
|
||||||
|
encoder.encodeString(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||||
import org.jellyfin.sdk.model.api.UserDto
|
import org.jellyfin.sdk.model.api.UserDto
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -167,6 +168,7 @@ class ServerRepository
|
||||||
suspend fun changeUser(
|
suspend fun changeUser(
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
authenticationResult: AuthenticationResult,
|
authenticationResult: AuthenticationResult,
|
||||||
|
existingUser: JellyfinUser?,
|
||||||
) = withContext(ioDispatcher) {
|
) = withContext(ioDispatcher) {
|
||||||
val accessToken = authenticationResult.accessToken
|
val accessToken = authenticationResult.accessToken
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
|
|
@ -183,12 +185,24 @@ class ServerRepository
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
val user =
|
val user =
|
||||||
authedUser?.let {
|
authedUser?.let {
|
||||||
JellyfinUser(
|
if (existingUser != null) {
|
||||||
id = it.id,
|
Timber.d("Re-using existing user")
|
||||||
name = it.name,
|
existingUser.copy(
|
||||||
serverId = server.id,
|
// If the server authenticated via the server, always remove the PIN
|
||||||
accessToken = accessToken,
|
pin = null,
|
||||||
)
|
accessToken = accessToken,
|
||||||
|
lastUsed = ZonedDateTime.now(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Timber.d("Creating new user")
|
||||||
|
JellyfinUser(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name,
|
||||||
|
serverId = server.id,
|
||||||
|
accessToken = accessToken,
|
||||||
|
lastUsed = ZonedDateTime.now(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
return@withContext changeUser(server, user)
|
return@withContext changeUser(server, user)
|
||||||
|
|
@ -253,11 +267,12 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setUserPin(
|
suspend fun updateUserAuth(
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
pin: String?,
|
pin: String?,
|
||||||
) = withContext(ioDispatcher) {
|
requireLogin: Boolean,
|
||||||
val newUser = user.copy(pin = pin)
|
) {
|
||||||
|
val newUser = user.copy(pin = pin, requireLogin = requireLogin)
|
||||||
val updatedUser = serverDao.addOrUpdateUser(newUser)
|
val updatedUser = serverDao.addOrUpdateUser(newUser)
|
||||||
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
|
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
|
||||||
// Updating current user, so push out the change
|
// Updating current user, so push out the change
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@file:UseSerializers(UUIDSerializer::class)
|
@file:UseSerializers(UUIDSerializer::class, ZonedDateTimeSerializer::class)
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.data.model
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
|
@ -10,11 +10,13 @@ import androidx.room.Ignore
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
import com.github.damontecres.wholphin.data.ZonedDateTimeSerializer
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.UseSerializers
|
import kotlinx.serialization.UseSerializers
|
||||||
import org.jellyfin.sdk.model.ServerVersion
|
import org.jellyfin.sdk.model.ServerVersion
|
||||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,6 +29,7 @@ data class JellyfinServer(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val url: String,
|
val url: String,
|
||||||
val version: String?,
|
val version: String?,
|
||||||
|
val lastUsed: ZonedDateTime? = null,
|
||||||
) {
|
) {
|
||||||
@get:Ignore
|
@get:Ignore
|
||||||
val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) }
|
val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) }
|
||||||
|
|
@ -58,11 +61,14 @@ data class JellyfinUser(
|
||||||
val serverId: UUID,
|
val serverId: UUID,
|
||||||
val accessToken: String?,
|
val accessToken: String?,
|
||||||
val pin: String? = null,
|
val pin: String? = null,
|
||||||
|
@ColumnInfo(defaultValue = "false")
|
||||||
|
val requireLogin: Boolean = false,
|
||||||
|
val lastUsed: ZonedDateTime? = null,
|
||||||
) {
|
) {
|
||||||
val hasPin: Boolean get() = pin.isNotNullOrBlank()
|
val hasPin: Boolean get() = pin.isNotNullOrBlank()
|
||||||
|
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})"
|
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, lastUsed=$lastUsed, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -914,9 +914,9 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
val RequireProfilePin =
|
val ProtectProfilePreference =
|
||||||
AppClickablePreference<AppPreferences>(
|
AppClickablePreference<AppPreferences>(
|
||||||
title = R.string.require_pin_code,
|
title = R.string.profile_protection,
|
||||||
)
|
)
|
||||||
|
|
||||||
val ImageDiskCacheSize =
|
val ImageDiskCacheSize =
|
||||||
|
|
@ -1088,7 +1088,7 @@ val basicPreferences =
|
||||||
title = R.string.profile_specific_settings,
|
title = R.string.profile_specific_settings,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.RequireProfilePin,
|
AppPreference.ProtectProfilePreference,
|
||||||
AppPreference.CustomizeHome,
|
AppPreference.CustomizeHome,
|
||||||
AppPreference.UserPinnedNavDrawerItems,
|
AppPreference.UserPinnedNavDrawerItems,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
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
|
||||||
|
|
@ -391,16 +390,45 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPreference.RequireProfilePin -> {
|
AppPreference.ProtectProfilePreference -> {
|
||||||
SwitchPreference(
|
val summary =
|
||||||
|
when {
|
||||||
|
currentUser?.requireLogin == true -> R.string.require_login
|
||||||
|
currentUser?.hasPin == true -> R.string.require_pin_code
|
||||||
|
else -> R.string.none
|
||||||
|
}
|
||||||
|
ChoicePreference(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
value = currentUser?.pin.isNotNullOrBlank(),
|
summary = stringResource(summary),
|
||||||
onClick = {
|
possibleValues =
|
||||||
showPinFlow = true
|
listOf(
|
||||||
|
stringResource(R.string.none),
|
||||||
|
stringResource(R.string.require_pin_code),
|
||||||
|
stringResource(R.string.require_login),
|
||||||
|
),
|
||||||
|
selectedIndex =
|
||||||
|
when {
|
||||||
|
currentUser?.requireLogin == true -> ProfileProtection.LOGIN
|
||||||
|
currentUser?.hasPin == true -> ProfileProtection.PIN
|
||||||
|
else -> ProfileProtection.NONE
|
||||||
|
}.ordinal,
|
||||||
|
onValueChange = {
|
||||||
|
currentUser?.let { user ->
|
||||||
|
when (ProfileProtection.entries[it]) {
|
||||||
|
ProfileProtection.NONE -> {
|
||||||
|
viewModel.removeLoginAndPin(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileProtection.PIN -> {
|
||||||
|
showPinFlow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileProtection.LOGIN -> {
|
||||||
|
viewModel.setRequireLogin(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
summaryOn = stringResource(R.string.enabled),
|
|
||||||
summaryOff = null,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,19 @@ class PreferencesViewModel
|
||||||
pin: String?,
|
pin: String?,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||||
serverRepository.setUserPin(user, pin)
|
serverRepository.updateUserAuth(user, pin, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRequireLogin(user: JellyfinUser) {
|
||||||
|
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||||
|
serverRepository.updateUserAuth(user, null, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLoginAndPin(user: JellyfinUser) {
|
||||||
|
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
|
||||||
|
serverRepository.updateUserAuth(user, null, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@ enum class PinFlowState {
|
||||||
CONFIRM_PIN,
|
CONFIRM_PIN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ProfileProtection {
|
||||||
|
NONE,
|
||||||
|
PIN,
|
||||||
|
LOGIN,
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SetPinFlow(
|
fun SetPinFlow(
|
||||||
currentPin: String?,
|
currentPin: String?,
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,19 @@ fun SwitchUserContent(
|
||||||
|
|
||||||
val currentUser by viewModel.serverRepository.currentUser.observeAsState()
|
val currentUser by viewModel.serverRepository.currentUser.observeAsState()
|
||||||
var showAddUser by remember { mutableStateOf(false) }
|
var showAddUser by remember { mutableStateOf(false) }
|
||||||
var username by remember(server) { mutableStateOf("") }
|
var addUser by remember(server) { mutableStateOf<JellyfinUser?>(null) }
|
||||||
|
var username by remember(addUser) { mutableStateOf(addUser?.name ?: "") }
|
||||||
|
|
||||||
fun showAddUserDialog(user: JellyfinUser?) {
|
fun showAddUserDialog(user: JellyfinUser?) {
|
||||||
username = user?.name ?: ""
|
addUser = user
|
||||||
showAddUser = true
|
showAddUser = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideAddUserDialog() {
|
||||||
|
addUser = null
|
||||||
|
showAddUser = false
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.switchUserState) {
|
LaunchedEffect(state.switchUserState) {
|
||||||
if (!showAddUser) {
|
if (!showAddUser) {
|
||||||
when (val s = state.switchUserState) {
|
when (val s = state.switchUserState) {
|
||||||
|
|
@ -136,7 +142,7 @@ fun SwitchUserContent(
|
||||||
users = state.users,
|
users = state.users,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
onSwitchUser = { user ->
|
onSwitchUser = { user ->
|
||||||
if (user.accessToken == null) {
|
if (user.accessToken == null || user.requireLogin) {
|
||||||
showAddUserDialog(user)
|
showAddUserDialog(user)
|
||||||
} else if (user.hasPin) {
|
} else if (user.hasPin) {
|
||||||
switchUserWithPin = user
|
switchUserWithPin = user
|
||||||
|
|
@ -174,13 +180,13 @@ fun SwitchUserContent(
|
||||||
viewModel.clearSwitchUserState()
|
viewModel.clearSwitchUserState()
|
||||||
viewModel.resetAttempts()
|
viewModel.resetAttempts()
|
||||||
if (useQuickConnect) {
|
if (useQuickConnect) {
|
||||||
viewModel.initiateQuickConnect(server)
|
viewModel.initiateQuickConnect(server, addUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BasicDialog(
|
BasicDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
viewModel.cancelQuickConnect()
|
viewModel.cancelQuickConnect()
|
||||||
showAddUser = false
|
hideAddUserDialog()
|
||||||
},
|
},
|
||||||
properties =
|
properties =
|
||||||
DialogProperties(
|
DialogProperties(
|
||||||
|
|
@ -244,6 +250,7 @@ fun SwitchUserContent(
|
||||||
val onSubmit = {
|
val onSubmit = {
|
||||||
viewModel.login(
|
viewModel.login(
|
||||||
server,
|
server,
|
||||||
|
addUser,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ class SwitchUserViewModel
|
||||||
|
|
||||||
fun login(
|
fun login(
|
||||||
server: JellyfinServer,
|
server: JellyfinServer,
|
||||||
|
existingUser: JellyfinUser?,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) {
|
) {
|
||||||
|
|
@ -149,7 +150,8 @@ class SwitchUserViewModel
|
||||||
username = username,
|
username = username,
|
||||||
password = password,
|
password = password,
|
||||||
)
|
)
|
||||||
val current = serverRepository.changeUser(server.url, authenticationResult)
|
val current =
|
||||||
|
serverRepository.changeUser(server.url, authenticationResult, existingUser)
|
||||||
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error logging in user")
|
Timber.e(ex, "Error logging in user")
|
||||||
|
|
@ -162,7 +164,10 @@ class SwitchUserViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initiateQuickConnect(server: JellyfinServer) {
|
fun initiateQuickConnect(
|
||||||
|
server: JellyfinServer,
|
||||||
|
existingUser: JellyfinUser?,
|
||||||
|
) {
|
||||||
quickConnectJob?.cancel()
|
quickConnectJob?.cancel()
|
||||||
quickConnectJob =
|
quickConnectJob =
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
|
@ -187,7 +192,12 @@ class SwitchUserViewModel
|
||||||
val authenticationResult by api.userApi.authenticateWithQuickConnect(
|
val authenticationResult by api.userApi.authenticateWithQuickConnect(
|
||||||
QuickConnectDto(secret = quickConnectStatus.secret),
|
QuickConnectDto(secret = quickConnectStatus.secret),
|
||||||
)
|
)
|
||||||
val current = serverRepository.changeUser(server.url, authenticationResult)
|
val current =
|
||||||
|
serverRepository.changeUser(
|
||||||
|
server.url,
|
||||||
|
authenticationResult,
|
||||||
|
existingUser,
|
||||||
|
)
|
||||||
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
// no-op, user may have canceled
|
// no-op, user may have canceled
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@
|
||||||
<string name="use_server_credentials">Login via server</string>
|
<string name="use_server_credentials">Login via server</string>
|
||||||
<string name="press_enter_to_confirm">Press center to confirm</string>
|
<string name="press_enter_to_confirm">Press center to confirm</string>
|
||||||
<string name="require_pin_code">Require PIN for profile</string>
|
<string name="require_pin_code">Require PIN for profile</string>
|
||||||
|
<string name="require_login">Require login for profile</string>
|
||||||
<string name="confirm_pin">Confirm PIN</string>
|
<string name="confirm_pin">Confirm PIN</string>
|
||||||
<string name="incorrect">Incorrect</string>
|
<string name="incorrect">Incorrect</string>
|
||||||
<string name="pin_too_short">PIN must be 4 keys or longer</string>
|
<string name="pin_too_short">PIN must be 4 keys or longer</string>
|
||||||
|
|
@ -778,6 +779,7 @@
|
||||||
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
||||||
<string name="play_with">Play with</string>
|
<string name="play_with">Play with</string>
|
||||||
<string name="transcoding">Transcoding</string>
|
<string name="transcoding">Transcoding</string>
|
||||||
|
<string name="profile_protection">Profile protection</string>
|
||||||
<string name="continue_watching_click_behavior">Continue watching/Next up click behavior</string>
|
<string name="continue_watching_click_behavior">Continue watching/Next up click behavior</string>
|
||||||
<string name="continue_watching_click_summary_on">Play on click</string>
|
<string name="continue_watching_click_summary_on">Play on click</string>
|
||||||
<string name="continue_watching_click_summary_off">Show details on click</string>
|
<string name="continue_watching_click_summary_off">Show details on click</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue