Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
659 changes: 659 additions & 0 deletions app/schemas/com.github.damontecres.wholphin.data.AppDatabase/32.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,11 @@ class MainActivityViewModel
appUpgradeHandler.copySubfont(false)
val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
val userHasPin = serverRepository.currentUser.value?.hasPin == true
if (prefs.signInAutomatically && !userHasPin) {
val allowAutoSignIn =
serverRepository.currentUser.value?.let {
!it.hasPin && !it.requireLogin
} == true
if (prefs.signInAutomatically && allowAutoSignIn) {
val current =
serverRepository.restoreSession(
prefs.currentServerId?.toUUIDOrNull(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.SeerrUser
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 org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.UUID

@Database(
Expand All @@ -40,7 +48,7 @@ import java.util.UUID
SeerrUser::class,

],
version = 31,
version = 32,
exportSchema = true,
autoMigrations = [
AutoMigration(3, 4),
Expand All @@ -55,6 +63,7 @@ import java.util.UUID
AutoMigration(12, 20),
AutoMigration(20, 30),
AutoMigration(30, 31),
AutoMigration(31, 32),
],
)
@TypeConverters(Converters::class)
Expand Down Expand Up @@ -116,6 +125,27 @@ class Converters {
Timber.e(ex, "Error parsing view options")
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.jellyfin.sdk.model.api.AuthenticationResult
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import java.time.ZonedDateTime
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -167,6 +168,7 @@ class ServerRepository
suspend fun changeUser(
serverUrl: String,
authenticationResult: AuthenticationResult,
existingUser: JellyfinUser?,
) = withContext(ioDispatcher) {
val accessToken = authenticationResult.accessToken
if (accessToken != null) {
Expand All @@ -183,12 +185,24 @@ class ServerRepository
if (server != null) {
val user =
authedUser?.let {
JellyfinUser(
id = it.id,
name = it.name,
serverId = server.id,
accessToken = accessToken,
)
if (existingUser != null) {
Timber.d("Re-using existing user")
existingUser.copy(
// If the server authenticated via the server, always remove the PIN
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) {
return@withContext changeUser(server, user)
Expand Down Expand Up @@ -253,11 +267,12 @@ class ServerRepository
}
}

suspend fun setUserPin(
suspend fun updateUserAuth(
user: JellyfinUser,
pin: String?,
) = withContext(ioDispatcher) {
val newUser = user.copy(pin = pin)
requireLogin: Boolean,
) {
val newUser = user.copy(pin = pin, requireLogin = requireLogin)
val updatedUser = serverDao.addOrUpdateUser(newUser)
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
// Updating current user, so push out the change
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:UseSerializers(UUIDSerializer::class)
@file:UseSerializers(UUIDSerializer::class, ZonedDateTimeSerializer::class)

package com.github.damontecres.wholphin.data.model

Expand All @@ -10,11 +10,13 @@ import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import com.github.damontecres.wholphin.data.ZonedDateTimeSerializer
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import java.time.ZonedDateTime
import java.util.UUID

/**
Expand All @@ -27,6 +29,7 @@ data class JellyfinServer(
val name: String?,
val url: String,
val version: String?,
val lastUsed: ZonedDateTime? = null,
) {
@get:Ignore
val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) }
Expand Down Expand Up @@ -58,11 +61,14 @@ data class JellyfinUser(
val serverId: UUID,
val accessToken: String?,
val pin: String? = null,
@ColumnInfo(defaultValue = "false")
val requireLogin: Boolean = false,
val lastUsed: ZonedDateTime? = null,
) {
val hasPin: Boolean get() = pin.isNotNullOrBlank()

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()})"
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,9 @@ sealed interface AppPreference<Pref, T> {
summaryOff = R.string.disabled,
)

val RequireProfilePin =
val ProtectProfilePreference =
AppClickablePreference<AppPreferences>(
title = R.string.require_pin_code,
title = R.string.profile_protection,
)

val ImageDiskCacheSize =
Expand Down Expand Up @@ -1088,7 +1088,7 @@ val basicPreferences =
title = R.string.profile_specific_settings,
preferences =
listOf(
AppPreference.RequireProfilePin,
AppPreference.ProtectProfilePreference,
AppPreference.CustomizeHome,
AppPreference.UserPinnedNavDrawerItems,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.ifElse
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.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus
Expand Down Expand Up @@ -391,16 +390,45 @@ fun PreferencesContent(
)
}

AppPreference.RequireProfilePin -> {
SwitchPreference(
AppPreference.ProtectProfilePreference -> {
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),
value = currentUser?.pin.isNotNullOrBlank(),
onClick = {
showPinFlow = true
summary = stringResource(summary),
possibleValues =
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,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,19 @@ class PreferencesViewModel
pin: String?,
) {
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ enum class PinFlowState {
CONFIRM_PIN,
}

enum class ProfileProtection {
NONE,
PIN,
LOGIN,
}

@Composable
fun SetPinFlow(
currentPin: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,19 @@ fun SwitchUserContent(

val currentUser by viewModel.serverRepository.currentUser.observeAsState()
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?) {
username = user?.name ?: ""
addUser = user
showAddUser = true
}

fun hideAddUserDialog() {
addUser = null
showAddUser = false
}

LaunchedEffect(state.switchUserState) {
if (!showAddUser) {
when (val s = state.switchUserState) {
Expand Down Expand Up @@ -136,7 +142,7 @@ fun SwitchUserContent(
users = state.users,
currentUser = currentUser,
onSwitchUser = { user ->
if (user.accessToken == null) {
if (user.accessToken == null || user.requireLogin) {
showAddUserDialog(user)
} else if (user.hasPin) {
switchUserWithPin = user
Expand Down Expand Up @@ -174,13 +180,13 @@ fun SwitchUserContent(
viewModel.clearSwitchUserState()
viewModel.resetAttempts()
if (useQuickConnect) {
viewModel.initiateQuickConnect(server)
viewModel.initiateQuickConnect(server, addUser)
}
}
BasicDialog(
onDismissRequest = {
viewModel.cancelQuickConnect()
showAddUser = false
hideAddUserDialog()
},
properties =
DialogProperties(
Expand Down Expand Up @@ -244,6 +250,7 @@ fun SwitchUserContent(
val onSubmit = {
viewModel.login(
server,
addUser,
username,
password,
)
Expand Down
Loading
Loading