diff --git a/app/schemas/dev.aaa1115910.bv.dao.AppDatabase/3.json b/app/schemas/dev.aaa1115910.bv.dao.AppDatabase/3.json
new file mode 100644
index 00000000..1b1a283f
--- /dev/null
+++ b/app/schemas/dev.aaa1115910.bv.dao.AppDatabase/3.json
@@ -0,0 +1,97 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "ad0905227bbe6c87b6048b4124cf310d",
+ "entities": [
+ {
+ "tableName": "search_history",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `keyword` TEXT NOT NULL, `search_date` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "keyword",
+ "columnName": "keyword",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "searchDate",
+ "columnName": "search_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "user",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `uid` INTEGER NOT NULL, `username` TEXT NOT NULL, `avatar` TEXT NOT NULL, `auth` TEXT NOT NULL, `lock` TEXT NOT NULL DEFAULT '')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "avatar",
+ "columnName": "avatar",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "auth",
+ "columnName": "auth",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lock",
+ "columnName": "lock",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "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, 'ad0905227bbe6c87b6048b4124cf310d')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dcfb6d49..dfdde6e0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,6 +30,11 @@
android:supportsRtl="true"
android:theme="@style/Theme.BV"
tools:ignore="UnusedAttribute">
+
+ logger.info { "unlock user lock for user ${user.uid}" }
+ userLockLocked = false
+ }
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/activities/user/UserLockSettingsActivity.kt b/app/src/main/kotlin/dev/aaa1115910/bv/activities/user/UserLockSettingsActivity.kt
new file mode 100644
index 00000000..b60f54a5
--- /dev/null
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/activities/user/UserLockSettingsActivity.kt
@@ -0,0 +1,34 @@
+package dev.aaa1115910.bv.activities.user
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import dev.aaa1115910.bv.screen.user.lock.UserLockSettingsScreen
+import dev.aaa1115910.bv.ui.theme.BVTheme
+
+class UserLockSettingsActivity : ComponentActivity() {
+
+ companion object {
+ fun actionStart(
+ context: Context,
+ uid: Long
+ ) {
+ context.startActivity(
+ Intent(context, UserLockSettingsActivity::class.java).apply {
+ putExtra("uid", uid)
+ }
+ )
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ BVTheme {
+ UserLockSettingsScreen()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt b/app/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt
index 7c6c70b7..1ea84752 100644
--- a/app/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt
@@ -16,10 +16,11 @@ import java.util.concurrent.Executors
@Database(
entities = [SearchHistoryDB::class, UserDB::class],
- version = 2,
+ version = 3,
exportSchema = true,
autoMigrations = [
- AutoMigration(from = 1, to = 2)
+ AutoMigration(from = 1, to = 2),
+ AutoMigration(from = 2, to = 3)
]
)
@TypeConverters(Converters::class)
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt b/app/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt
index bc0bdafa..c30ff33e 100644
--- a/app/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt
@@ -5,10 +5,12 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "user")
+
data class UserDB(
@PrimaryKey(autoGenerate = true) val id: Int? = null,
@ColumnInfo(name = "uid") val uid: Long,
@ColumnInfo(name = "username") var username: String,
@ColumnInfo(name = "avatar") var avatar: String,
- @ColumnInfo(name = "auth") var auth: String
+ @ColumnInfo(name = "auth") var auth: String,
+ @ColumnInfo(name = "lock", defaultValue = "") var lock: String = "",
)
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt b/app/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt
index c67dbc51..16d7c621 100644
--- a/app/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt
@@ -172,4 +172,12 @@ class UserRepository(
avatar = it.avatar
}
}
+
+ suspend fun findUserByUid(uid: Long): UserDB? {
+ return db.userDao().findUserByUid(uid)
+ }
+
+ suspend fun updateUser(user: UserDB){
+ db.userDao().update(user)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/UserSwitchScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/UserSwitchScreen.kt
index a44b1dac..eb35b1c4 100644
--- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/UserSwitchScreen.kt
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/UserSwitchScreen.kt
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -23,9 +22,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.BadgedBox
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@@ -48,8 +50,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewModelScope
+import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Button
@@ -60,14 +66,18 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.OutlinedButton
import androidx.tv.material3.Surface
+import androidx.tv.material3.SurfaceDefaults
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import dev.aaa1115910.bv.BVApp
import dev.aaa1115910.bv.R
import dev.aaa1115910.bv.activities.user.LoginActivity
+import dev.aaa1115910.bv.activities.user.UserLockSettingsActivity
+import dev.aaa1115910.bv.component.ifElse
import dev.aaa1115910.bv.dao.AppDatabase
import dev.aaa1115910.bv.entity.db.UserDB
import dev.aaa1115910.bv.repository.UserRepository
+import dev.aaa1115910.bv.screen.user.lock.UnlockSwitchUserContent
import dev.aaa1115910.bv.ui.theme.BVTheme
import dev.aaa1115910.bv.util.requestFocus
import io.github.g0dkar.qrcode.QRCode
@@ -75,41 +85,99 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.getKoin
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@Composable
fun UserSwitchScreen(
modifier: Modifier = Modifier,
- userSwitchViewModel: UserSwitchViewModel = koinViewModel()
+ userSwitchViewModel: UserSwitchViewModel = koinViewModel(),
+ userRepository: UserRepository = getKoin().get()
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
+ val lifecycleOwner = LocalLifecycleOwner.current
+
val userList = userSwitchViewModel.userDbList
+ var showUnlock by remember { mutableStateOf(false) }
+ var unlockUser: UserDB? by remember { mutableStateOf(null) }
+
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ scope.launch {
+ //userSwitchViewModel.updateUserDbList()
+ userSwitchViewModel.updateData()
+ }
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ val unlockFocusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(showUnlock) {
+ if (showUnlock) unlockFocusRequester.requestFocus()
+ }
+
Surface(
modifier = modifier,
shape = RoundedCornerShape(0.dp)
) {
- UserSwitchContent(
- userList = userList,
- loadingUserList = userSwitchViewModel.loading,
- onAddUser = {
- context.startActivity(Intent(context, LoginActivity::class.java))
- },
- onDeleteUser = { user ->
- scope.launch(Dispatchers.IO) {
- userSwitchViewModel.deleteUser(user)
- if (userList.isEmpty()) (context as Activity).finish()
- }
- },
- onSwitchUser = { user ->
- scope.launch(Dispatchers.IO) {
- userSwitchViewModel.switchUser(user)
- (context as Activity).finish()
+ Box {
+ UserSwitchContent(
+ userList = userList,
+ currentUid = userRepository.uid,
+ loadingUserList = userSwitchViewModel.loading,
+ onAddUser = {
+ context.startActivity(Intent(context, LoginActivity::class.java))
+ },
+ onDeleteUser = { user ->
+ scope.launch(Dispatchers.IO) {
+ userSwitchViewModel.deleteUser(user)
+ if (userList.isEmpty()) (context as Activity).finish()
+ }
+ },
+ onSwitchUser = { user ->
+ if (user.uid != userRepository.uid && user.lock.isNotBlank()) {
+ unlockUser = user
+ showUnlock = true
+ } else {
+ scope.launch(Dispatchers.IO) {
+ userSwitchViewModel.switchUser(user)
+ (context as Activity).finish()
+ }
+ }
+ },
+ onShowUserLockSettings = { uid ->
+ UserLockSettingsActivity.actionStart(context, uid)
}
+ )
+
+ if (showUnlock) {
+ UnlockSwitchUserContent(
+ modifier = Modifier.focusRequester(unlockFocusRequester),
+ userList = userList,
+ unlockUser = unlockUser!!,
+ onUnlockSuccess = { user ->
+ scope.launch(Dispatchers.IO) {
+ userSwitchViewModel.switchUser(user)
+ (context as Activity).finish()
+ }
+ },
+ onCancel = {
+ showUnlock = false
+ }
+ )
}
- )
+ }
}
}
@@ -117,13 +185,15 @@ fun UserSwitchScreen(
private fun UserSwitchContent(
modifier: Modifier = Modifier,
userList: List = emptyList(),
+ currentUid: Long,
loadingUserList: Boolean,
onSwitchUser: (UserDB) -> Unit,
onDeleteUser: (UserDB) -> Unit,
- onAddUser: () -> Unit
+ onAddUser: () -> Unit,
+ onShowUserLockSettings: (Long) -> Unit
) {
val focusRequester = remember { FocusRequester() }
- var currentUser by remember {
+ var choosedUser by remember {
mutableStateOf(
UserDB(
uid = -1,
@@ -176,9 +246,10 @@ private fun UserSwitchContent(
UserItem(
avatar = user.avatar,
username = user.username,
+ lockEnabled = user.lock.isNotBlank(),
onClick = {
if (isInManagerMode) {
- currentUser = user
+ choosedUser = user
showUserMenuDialog = true
} else {
onSwitchUser(user)
@@ -228,23 +299,29 @@ private fun UserSwitchContent(
UserMenuDialog(
show = showUserMenuDialog,
onHideDialog = { showUserMenuDialog = false },
- username = currentUser.username,
+ username = choosedUser.username,
+ uid = choosedUser.uid,
+ showTokenButton = choosedUser.uid == currentUid || choosedUser.lock.isBlank(),
onShowUserAuthData = { showAuthDataDialog = true },
- onDeleteUser = { showDeleteConfirmDialog = true }
+ onDeleteUser = { showDeleteConfirmDialog = true },
+ onShowUserLockSettings = { uid ->
+ isInManagerMode = false
+ onShowUserLockSettings(uid)
+ }
)
UserAuthDataDialog(
show = showAuthDataDialog,
onHideDialog = { showAuthDataDialog = false },
- userDB = currentUser
+ userDB = choosedUser
)
DeleteConfirmDialog(
show = showDeleteConfirmDialog,
onHideDialog = { showDeleteConfirmDialog = false },
- userDB = currentUser,
+ userDB = choosedUser,
onConfirm = {
- onDeleteUser(currentUser)
+ onDeleteUser(choosedUser)
showDeleteConfirmDialog = false
}
)
@@ -256,8 +333,11 @@ fun UserMenuDialog(
show: Boolean,
onHideDialog: () -> Unit,
username: String,
+ uid: Long,
+ showTokenButton: Boolean,
onShowUserAuthData: () -> Unit,
- onDeleteUser: () -> Unit
+ onDeleteUser: () -> Unit,
+ onShowUserLockSettings: (Long) -> Unit
) {
val menuFocusRequester = remember { FocusRequester() }
@@ -273,57 +353,49 @@ fun UserMenuDialog(
onDismissRequest = onHideDialog,
title = { Text(text = username) },
text = {
- Column(
+ TvLazyColumn(
modifier = Modifier.width(240.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(horizontal = 12.dp)
) {
- Button(
- modifier = Modifier
- .focusRequester(menuFocusRequester)
- .fillMaxWidth()
- .height(64.dp)
- .padding(horizontal = 12.dp),
- shape = ButtonDefaults.shape(
- shape = MaterialTheme.shapes.medium
- ),
- onClick = {
- onHideDialog()
- onShowUserAuthData()
- }
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Text(text = stringResource(R.string.user_switch_menu_show_token))
- }
- }
- Button(
- modifier = Modifier
- .fillMaxWidth()
- .height(64.dp)
- .padding(horizontal = 12.dp),
- shape = ButtonDefaults.shape(
- shape = MaterialTheme.shapes.medium
- ),
- colors = ButtonDefaults.colors(
- containerColor = MaterialTheme.colorScheme.errorContainer
- ),
- onClick = {
- onHideDialog()
- onDeleteUser()
- }
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = stringResource(R.string.user_switch_menu_delete_account),
- style = MaterialTheme.typography.bodyLarge
+ if (showTokenButton) {
+ item {
+ UserMenuButton(
+ modifier = Modifier.focusRequester(menuFocusRequester),
+ text = stringResource(R.string.user_switch_menu_show_token),
+ onClick = {
+ onHideDialog()
+ onShowUserAuthData()
+ }
)
}
}
+
+ item {
+ UserMenuButton(
+ modifier = Modifier
+ .ifElse(
+ !showTokenButton,
+ Modifier.focusRequester(menuFocusRequester)
+ ),
+ text = stringResource(R.string.user_switch_menu_user_lock),
+ onClick = {
+ onHideDialog()
+ onShowUserLockSettings(uid)
+ }
+ )
+ }
+
+ item {
+ UserMenuButton(
+ text = stringResource(R.string.user_switch_menu_delete_account),
+ onClick = {
+ onHideDialog()
+ onDeleteUser()
+ },
+ color = MaterialTheme.colorScheme.errorContainer
+ )
+ }
}
},
dismissButton = {},
@@ -436,44 +508,74 @@ private fun DeleteConfirmDialog(
}
@Composable
-private fun UserItem(
+fun UserItem(
modifier: Modifier = Modifier,
avatar: String,
username: String,
- onClick: () -> Unit
+ lockEnabled: Boolean = false,
+ onClick: (() -> Unit)? = null
) {
Column(
modifier = modifier.width(120.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Surface(
- modifier = Modifier
- .size(80.dp),
- colors = ClickableSurfaceDefaults.colors(
- containerColor = Color.DarkGray,
- focusedContainerColor = Color.Gray
- ),
- shape = ClickableSurfaceDefaults.shape(
+ if (onClick != null) {
+ BadgedBox(
+ modifier = Modifier.padding(18.dp),
+ badge = {
+ if (lockEnabled) {
+ Icon(imageVector = Icons.Default.Lock, contentDescription = null)
+ }
+ }
+ ) {
+ Surface(
+ modifier = Modifier
+ .size(80.dp),
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.DarkGray,
+ focusedContainerColor = Color.Gray
+ ),
+ shape = ClickableSurfaceDefaults.shape(
+ shape = CircleShape
+ ),
+ glow = ClickableSurfaceDefaults.glow(
+ focusedGlow = Glow(
+ elevationColor = MaterialTheme.colorScheme.inverseSurface,
+ elevation = 16.dp
+ )
+ ),
+ onClick = onClick
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(CircleShape),
+ model = avatar,
+ contentDescription = null,
+ contentScale = ContentScale.FillBounds
+ )
+ }
+ }
+ } else {
+ Surface(
+ modifier = Modifier
+ .padding(18.dp)
+ .size(80.dp),
+ colors = SurfaceDefaults.colors(
+ containerColor = Color.DarkGray
+ ),
shape = CircleShape
- ),
- glow = ClickableSurfaceDefaults.glow(
- focusedGlow = Glow(
- elevationColor = MaterialTheme.colorScheme.inverseSurface,
- elevation = 16.dp
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(80.dp)
+ .clip(CircleShape),
+ model = avatar,
+ contentDescription = null,
+ contentScale = ContentScale.FillBounds
)
- ),
- onClick = onClick
- ) {
- AsyncImage(
- modifier = Modifier
- .size(80.dp)
- .clip(CircleShape),
- model = avatar,
- contentDescription = null,
- contentScale = ContentScale.FillBounds
- )
+ }
}
- Spacer(modifier = Modifier.height(18.dp))
Box(
modifier = Modifier.height(26.dp),
contentAlignment = Alignment.Center
@@ -502,6 +604,7 @@ private fun AddUserItem(
) {
Surface(
modifier = Modifier
+ .padding(18.dp)
.size(80.dp),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color.DarkGray,
@@ -529,7 +632,6 @@ private fun AddUserItem(
)
}
}
- Spacer(modifier = Modifier.height(18.dp))
Box(
modifier = Modifier.height(26.dp),
contentAlignment = Alignment.Center
@@ -554,7 +656,8 @@ fun UserItemPreview() {
UserItem(
avatar = "",
username = "This is a user name",
- onClick = {}
+ onClick = {},
+ lockEnabled = true
)
}
}
@@ -585,7 +688,8 @@ fun UserSwitchContentPreview() {
uid = 1,
username = "This is a long username",
avatar = "0https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg",
- auth = "{xxx2}"
+ auth = "{xxx2}",
+ lock = "rdrd"
),
UserDB(
uid = 2,
@@ -594,10 +698,12 @@ fun UserSwitchContentPreview() {
auth = "{xxx3}"
)
),
+ currentUid = 0L,
loadingUserList = false,
onSwitchUser = {},
onDeleteUser = {},
- onAddUser = {}
+ onAddUser = {},
+ onShowUserLockSettings = {}
)
}
}
@@ -610,8 +716,11 @@ fun UserMenuDialogPreview() {
show = true,
onHideDialog = {},
username = "This is a user name",
+ uid = 0,
+ showTokenButton = true,
onShowUserAuthData = {},
- onDeleteUser = {}
+ onDeleteUser = {},
+ onShowUserLockSettings = {}
)
}
}
@@ -640,14 +749,14 @@ class UserSwitchViewModel(
var loading by mutableStateOf(true)
val userDbList = mutableStateListOf()
- init {
+ fun updateData() {
viewModelScope.launch(Dispatchers.IO) {
updateUserDbList()
withContext(Dispatchers.Main) { loading = false }
}
}
- suspend fun updateUserDbList() {
+ private suspend fun updateUserDbList() {
withContext(Dispatchers.Main) {
userDbList.clear()
userDbList.addAll(db.userDao().getAll())
@@ -667,4 +776,31 @@ class UserSwitchViewModel(
userRepository.logout()
}
}
+}
+
+@Composable
+private fun UserMenuButton(
+ modifier: Modifier = Modifier,
+ text: String,
+ onClick: () -> Unit,
+ color: Color? = null
+) {
+ Button(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = ButtonDefaults.shape(shape = MaterialTheme.shapes.medium),
+ colors = if (color != null) ButtonDefaults.colors(containerColor = color) else ButtonDefaults.colors(),
+ onClick = onClick
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockSwitchUserContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockSwitchUserContent.kt
new file mode 100644
index 00000000..0de0c3f8
--- /dev/null
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockSwitchUserContent.kt
@@ -0,0 +1,161 @@
+package dev.aaa1115910.bv.screen.user.lock
+
+import android.view.KeyEvent
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.items
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Surface
+import androidx.tv.material3.Text
+import dev.aaa1115910.bv.R
+import dev.aaa1115910.bv.component.ifElse
+import dev.aaa1115910.bv.entity.db.UserDB
+import dev.aaa1115910.bv.screen.user.UserItem
+import dev.aaa1115910.bv.util.toast
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+fun UnlockSwitchUserContent(
+ modifier: Modifier = Modifier,
+ userList: List,
+ unlockUser: UserDB?,
+ onUnlockSuccess: (UserDB) -> Unit,
+ onCancel: () -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val defaultFocusRequester = remember { FocusRequester() }
+ var inputPassword by remember { mutableStateOf("") }
+ val inputShow by remember {
+ derivedStateOf {
+ inputPassword
+ .replace("u", "*")
+ .replace("d", "*")
+ .replace("l", "*")
+ .replace("r", "*")
+ }
+ }
+ val unselectedUserAlpha by remember { mutableFloatStateOf(0.4f) }
+
+ LaunchedEffect(Unit) {
+ scope.launch {
+ delay(200)
+ println("request default focus")
+ defaultFocusRequester.requestFocus()
+ }
+ }
+
+ BackHandler(true) {
+ }
+
+ Surface(
+ modifier = modifier
+ .clickable {}
+ .focusRequester(defaultFocusRequester)
+ .onPreviewKeyEvent {
+ if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent true
+ when (it.key) {
+ Key.DirectionUp -> inputPassword += "u"
+ Key.DirectionDown -> inputPassword += "d"
+ Key.DirectionLeft -> inputPassword += "l"
+ Key.DirectionRight -> inputPassword += "r"
+ Key.DirectionCenter -> {
+ if (unlockUser?.lock == inputPassword) {
+ onUnlockSuccess(unlockUser)
+ } else {
+ R.string.user_lock_toast_password_error.toast(context)
+ inputPassword = ""
+ }
+ }
+
+ Key.Back -> {
+ if (inputPassword.isNotBlank()) {
+ inputPassword = inputPassword.drop(1)
+ } else {
+ onCancel()
+ }
+ }
+ }
+ return@onPreviewKeyEvent true
+ },
+ shape = RoundedCornerShape(0.dp)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = 64.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.user_lock_title_input_password),
+ style = MaterialTheme.typography.displaySmall
+ )
+ }
+
+ TvLazyRow(
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ contentPadding = PaddingValues(horizontal = 12.dp)
+ ) {
+ items(items = userList) { user ->
+ UserItem(
+ modifier = Modifier
+ .ifElse({ user != unlockUser }, Modifier.alpha(unselectedUserAlpha)),
+ avatar = user.avatar,
+ username = user.username,
+ lockEnabled = user.lock.isNotBlank(),
+ )
+ }
+ }
+
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 96.dp),
+ text = inputShow,
+ style = MaterialTheme.typography.displayLarge
+ )
+
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 48.dp),
+ text = stringResource(R.string.user_lock_input_tip),
+ color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockUserScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockUserScreen.kt
new file mode 100644
index 00000000..fa1f2500
--- /dev/null
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UnlockUserScreen.kt
@@ -0,0 +1,236 @@
+package dev.aaa1115910.bv.screen.user.lock
+
+import android.view.KeyEvent
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.items
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Surface
+import androidx.tv.material3.Text
+import dev.aaa1115910.bv.R
+import dev.aaa1115910.bv.component.ifElse
+import dev.aaa1115910.bv.entity.db.UserDB
+import dev.aaa1115910.bv.screen.user.UserItem
+import dev.aaa1115910.bv.screen.user.UserSwitchViewModel
+import dev.aaa1115910.bv.util.toast
+import io.github.oshai.kotlinlogging.KotlinLogging
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun UnlockUserScreen(
+ modifier: Modifier = Modifier,
+ userSwitchViewModel: UserSwitchViewModel = koinViewModel(),
+ onUnlockSuccess: (UserDB) -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val userList = userSwitchViewModel.userDbList
+ var selectedUser: UserDB? by remember { mutableStateOf(null) }
+
+ LaunchedEffect(Unit) {
+ userSwitchViewModel.updateData()
+ }
+
+ UnlockUserContent(
+ modifier = modifier,
+ userList = userList,
+ selectedUser = selectedUser,
+ onSelectedUserChange = { user ->
+ selectedUser = user
+ },
+ onUnlockSuccess = { user ->
+ scope.launch {
+ userSwitchViewModel.switchUser(user)
+ onUnlockSuccess(user)
+ }
+ }
+ )
+}
+
+@Composable
+private fun UnlockUserContent(
+ modifier: Modifier = Modifier,
+ userList: List,
+ selectedUser: UserDB?,
+ onSelectedUserChange: (UserDB) -> Unit,
+ onUnlockSuccess: (UserDB) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val logger = KotlinLogging.logger("UnlockUserContent")
+
+ val inputFocusRequester = remember { FocusRequester() }
+ val defaultFocusRequester = remember { FocusRequester() }
+ var inputPassword by remember { mutableStateOf("") }
+ val inputShow by remember {
+ derivedStateOf {
+ inputPassword
+ .replace("u", "*")
+ .replace("d", "*")
+ .replace("l", "*")
+ .replace("r", "*")
+ }
+ }
+ var unlockState by remember { mutableStateOf(UnlockState.ChooseUser) }
+ val unChosenUserAlpha by animateFloatAsState(
+ targetValue = when (unlockState) {
+ UnlockState.ChooseUser -> 1f
+ UnlockState.InputPassword -> 0.4f
+ },
+ label = "unchosen user alpha"
+ )
+
+ LaunchedEffect(userList) {
+ scope.launch {
+ delay(200)
+ defaultFocusRequester.requestFocus()
+ }
+ }
+
+ LaunchedEffect(unlockState) {
+ scope.launch {
+ delay(100)
+ inputFocusRequester.requestFocus()
+ }
+ }
+
+ BackHandler(true) {
+ }
+
+ Surface(
+ modifier = modifier
+ .ifElse({ unlockState == UnlockState.InputPassword }, Modifier.clickable {})
+ .focusRequester(inputFocusRequester)
+ .onPreviewKeyEvent {
+ when (unlockState) {
+ UnlockState.ChooseUser -> return@onPreviewKeyEvent false
+ UnlockState.InputPassword -> {
+ if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent true
+ when (it.key) {
+ Key.DirectionUp -> inputPassword += "u"
+ Key.DirectionDown -> inputPassword += "d"
+ Key.DirectionLeft -> inputPassword += "l"
+ Key.DirectionRight -> inputPassword += "r"
+ Key.DirectionCenter -> {
+ if (selectedUser?.lock == inputPassword) {
+ onUnlockSuccess(selectedUser)
+ } else {
+ "密码错误".toast(context)
+ inputPassword = ""
+ }
+ }
+
+ Key.Back -> {
+ if (inputPassword.isNotBlank()) {
+ inputPassword = inputPassword.drop(1)
+ } else {
+ unlockState = UnlockState.ChooseUser
+ defaultFocusRequester.requestFocus()
+ }
+ }
+ }
+ return@onPreviewKeyEvent true
+ }
+ }
+ },
+ shape = RoundedCornerShape(0.dp)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = 64.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = when (unlockState) {
+ UnlockState.ChooseUser -> stringResource(R.string.user_lock_title_choose_user)
+ UnlockState.InputPassword -> stringResource(R.string.user_lock_title_input_password)
+ },
+ style = MaterialTheme.typography.displaySmall
+ )
+ }
+
+ TvLazyRow(
+ modifier = Modifier.focusRequester(defaultFocusRequester),
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ contentPadding = PaddingValues(horizontal = 12.dp)
+ ) {
+ items(items = userList) { user ->
+ UserItem(
+ modifier = Modifier
+ .ifElse({ user != selectedUser }, Modifier.alpha(unChosenUserAlpha)),
+ avatar = user.avatar,
+ username = user.username,
+ lockEnabled = user.lock.isNotBlank(),
+ onClick = {
+ logger.info { "Choose user ${user.uid}" }
+ if (user.lock.isNotBlank()) {
+ onSelectedUserChange(user)
+ unlockState = UnlockState.InputPassword
+ } else {
+ onSelectedUserChange(user)
+ onUnlockSuccess(user)
+ }
+ }.takeIf { unlockState == UnlockState.ChooseUser }
+ )
+ }
+ }
+
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 96.dp),
+ text = inputShow,
+ style = MaterialTheme.typography.displayLarge
+ )
+
+ if (unlockState == UnlockState.InputPassword) {
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 48.dp),
+ text = stringResource(R.string.user_lock_input_tip),
+ color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
+ )
+ }
+ }
+ }
+}
+
+private enum class UnlockState {
+ ChooseUser,
+ InputPassword
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UserLockSettingsScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UserLockSettingsScreen.kt
new file mode 100644
index 00000000..6192617f
--- /dev/null
+++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/user/lock/UserLockSettingsScreen.kt
@@ -0,0 +1,247 @@
+package dev.aaa1115910.bv.screen.user.lock
+
+import android.app.Activity
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Surface
+import androidx.tv.material3.Text
+import dev.aaa1115910.bv.R
+import dev.aaa1115910.bv.entity.db.UserDB
+import dev.aaa1115910.bv.repository.UserRepository
+import dev.aaa1115910.bv.screen.user.UserItem
+import dev.aaa1115910.bv.util.toast
+import io.github.oshai.kotlinlogging.KotlinLogging
+import kotlinx.coroutines.launch
+import org.koin.compose.getKoin
+
+@Composable
+fun UserLockSettingsScreen(
+ modifier: Modifier = Modifier,
+ userRepository: UserRepository = getKoin().get()
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val logger = KotlinLogging.logger("UserLockSettingsScreen")
+
+ var user by remember {
+ mutableStateOf(
+ UserDB(
+ uid = -1,
+ username = "None",
+ avatar = "",
+ auth = ""
+ )
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ val intent = (context as Activity).intent
+ if (intent.hasExtra("uid")) {
+ val uid = intent.getLongExtra("uid", 0)
+ userRepository.findUserByUid(uid)
+ ?.let { user = it }
+ ?: let { context.finish() }
+ logger.debug { "user $uid lock: ${user.lock}" }
+ } else {
+ context.finish()
+ }
+ }
+
+ UserLockSettingsContent(
+ modifier = modifier,
+ user = user,
+ onUpdateUser = {
+ scope.launch {
+ userRepository.updateUser(it)
+ (context as Activity).finish()
+ }
+ },
+ onExit = {
+ (context as Activity).finish()
+ }
+ )
+}
+
+@Composable
+private fun UserLockSettingsContent(
+ modifier: Modifier = Modifier,
+ user: UserDB,
+ onUpdateUser: (UserDB) -> Unit,
+ onExit: () -> Unit
+) {
+ val context = LocalContext.current
+
+ val focusRequester = remember { FocusRequester() }
+ var inputState by remember { mutableStateOf(InputState.InputOldPassword) }
+ var inputPassword by remember { mutableStateOf("") }
+ var lastInput by remember { mutableStateOf("") }
+ val inputShow by remember {
+ derivedStateOf {
+ inputPassword
+ .replace("u", "↑")
+ .replace("d", "↓")
+ .replace("l", "←")
+ .replace("r", "→")
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+
+ }
+ LaunchedEffect(user) {
+ inputState = if (user.lock.isNotBlank()) InputState.InputOldPassword
+ else InputState.InputNewPassword
+ }
+
+ BackHandler(inputPassword.isNotEmpty()) {
+ }
+
+ Surface(
+ modifier = modifier
+ .clickable {}
+ .focusRequester(focusRequester)
+ .onPreviewKeyEvent { keyEvent ->
+ if (keyEvent.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN) {
+ return@onPreviewKeyEvent true
+ }
+
+ when (keyEvent.key) {
+ Key.DirectionUp -> inputPassword += "u"
+ Key.DirectionDown -> inputPassword += "d"
+ Key.DirectionLeft -> inputPassword += "l"
+ Key.DirectionRight -> inputPassword += "r"
+
+ Key.DirectionCenter -> {
+ when (inputState) {
+ InputState.InputOldPassword -> {
+ if (inputPassword == user.lock) {
+ inputState = InputState.InputNewPassword
+ inputPassword = ""
+ } else {
+ R.string.user_lock_toast_password_error.toast(context)
+ inputPassword = ""
+ }
+ }
+
+ InputState.InputNewPassword -> {
+ if (inputPassword.isBlank()) {
+ R.string.user_lock_toast_password_removed.toast(context)
+ user.lock = ""
+ onUpdateUser(user)
+ } else {
+ lastInput = inputPassword
+ inputPassword = ""
+ inputState = InputState.ConfirmNewPassword
+ }
+ }
+
+ InputState.ConfirmNewPassword -> {
+ if (inputPassword == lastInput) {
+ user.lock = inputPassword
+ onUpdateUser(user)
+ } else {
+ R.string.user_lock_toast_password_different.toast(context)
+ inputPassword = ""
+ inputState = InputState.InputNewPassword
+ }
+ }
+ }
+ }
+
+ Key.Back -> {
+ if (inputPassword.isNotEmpty()) {
+ inputPassword = inputPassword.dropLast(1)
+ } else {
+ onExit()
+ }
+ }
+ }
+ true
+ },
+ shape = RoundedCornerShape(0.dp)
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = 64.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = when (inputState) {
+ InputState.InputOldPassword -> stringResource(R.string.user_lock_title_input_old_password)
+ InputState.InputNewPassword -> stringResource(R.string.user_lock_title_input_new_password)
+ InputState.ConfirmNewPassword -> stringResource(R.string.user_lock_title_input_new_password_again)
+ },
+ style = MaterialTheme.typography.displaySmall
+ )
+ }
+
+ TvLazyRow(
+ modifier = Modifier.focusRequester(focusRequester),
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ contentPadding = PaddingValues(horizontal = 12.dp)
+ ) {
+ item {
+ UserItem(
+ avatar = user.avatar,
+ username = user.username
+ )
+ }
+ }
+
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 96.dp),
+ text = inputShow,
+ style = MaterialTheme.typography.displayLarge
+ )
+
+ Text(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 48.dp),
+ text = stringResource(R.string.user_lock_input_tip),
+ color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
+ )
+ }
+ }
+}
+
+private enum class InputState {
+ InputOldPassword,
+ InputNewPassword,
+ ConfirmNewPassword
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index beb1e200..0dde2afc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -266,6 +266,7 @@
视频标签
UP 投稿
用户信息
+ 用户锁设置
用户切换
视频信息
视频播放
@@ -288,11 +289,21 @@
隐身模式
Lv%1$s
UID: %1$s
+ 方向键输入密码 / Center 键确认输入 / 返回键退格
+ 选择账户
+ 請輸入新密碼
+ 請再次輸入新密碼
+ 請輸入舊密碼
+ 输入密码
+ 密码不一致
+ 密码错误
+ 密码已移除
添加账号
退出管理
管理账号
移除账号
显示 Token
+ 用户锁设置
选择账号
默认