diff --git a/.gitignore b/.gitignore index a8b0d1d..7a88ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,20 @@ google-services.json # Android Profiling *.hprof + +# Test screenshots and debug files +*_test.png +screenshot*.png +frame_*.png +ss_*.png +ui*.xml +ui_dump.xml +ui_capsule.xml +# Common debug screenshots +screen.png +screen2.png +current.png +home_screen.png +t1.png +t2.png +t3.png diff --git a/app/src/main/java/com/example/myapplication/data/AppDatabase.java b/app/src/main/java/com/example/myapplication/data/AppDatabase.java index 6d9d660..69be728 100644 --- a/app/src/main/java/com/example/myapplication/data/AppDatabase.java +++ b/app/src/main/java/com/example/myapplication/data/AppDatabase.java @@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; @Database( entities = {IconLibraryEntity.class, ServerEntity.class}, - version = 4, + version = 5, exportSchema = false ) public abstract class AppDatabase extends RoomDatabase { @@ -69,7 +69,14 @@ public abstract class AppDatabase extends RoomDatabase { } }; - public static AppDatabase getInstance(Context context) { + private static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(SupportSQLiteDatabase db) { + db.execSQL("ALTER TABLE servers ADD COLUMN isSelected INTEGER NOT NULL DEFAULT 0"); + } + }; + +public static AppDatabase getInstance(Context context) { if (instance == null) { synchronized (AppDatabase.class) { if (instance == null) { @@ -77,11 +84,10 @@ public abstract class AppDatabase extends RoomDatabase { context.getApplicationContext(), AppDatabase.class, "emby_player.db" - ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build(); + ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5).build(); } } } return instance; } } - diff --git a/app/src/main/java/com/example/myapplication/data/ServerDao.java b/app/src/main/java/com/example/myapplication/data/ServerDao.java index 07d3275..167d357 100644 --- a/app/src/main/java/com/example/myapplication/data/ServerDao.java +++ b/app/src/main/java/com/example/myapplication/data/ServerDao.java @@ -22,4 +22,10 @@ public interface ServerDao { void deleteLegacySampleServers(); @Query("DELETE FROM servers WHERE id = :serverId") void deleteServer(String serverId); -} + + @Query("UPDATE servers SET isSelected = 0") + void clearAllSelected(); + + @Query("UPDATE servers SET isSelected = 1 WHERE id = :serverId") + void setSelected(String serverId); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/data/ServerEntity.java b/app/src/main/java/com/example/myapplication/data/ServerEntity.java index bcf0c8d..41e4ec0 100644 --- a/app/src/main/java/com/example/myapplication/data/ServerEntity.java +++ b/app/src/main/java/com/example/myapplication/data/ServerEntity.java @@ -20,6 +20,7 @@ public class ServerEntity { public String accessToken; public String userId; public String iconUrl; + public int isSelected; @Ignore public ServerEntity( @@ -34,9 +35,10 @@ public class ServerEntity { String accessToken, String userId ) { - this(id, name, host, basePath, port, username, password, note, accessToken, userId, ""); + this(id, name, host, basePath, port, username, password, note, accessToken, userId, "", 0); } + @Ignore public ServerEntity( String id, String name, @@ -49,6 +51,23 @@ public class ServerEntity { String accessToken, String userId, String iconUrl + ) { + this(id, name, host, basePath, port, username, password, note, accessToken, userId, iconUrl, 0); + } + + public ServerEntity( + String id, + String name, + String host, + String basePath, + String port, + String username, + String password, + String note, + String accessToken, + String userId, + String iconUrl, + int isSelected ) { this.id = id; this.name = name; @@ -61,5 +80,6 @@ public class ServerEntity { this.accessToken = accessToken; this.userId = userId; this.iconUrl = iconUrl == null ? "" : iconUrl; + this.isSelected = isSelected; } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/example/myapplication/MainActivity.kt b/app/src/main/kotlin/com/example/myapplication/MainActivity.kt index 831c27e..87ab7c0 100644 --- a/app/src/main/kotlin/com/example/myapplication/MainActivity.kt +++ b/app/src/main/kotlin/com/example/myapplication/MainActivity.kt @@ -11,12 +11,19 @@ import com.example.myapplication.ui.AppLanguage class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + val splashScreen = installSplashScreen() + var dataReady = false + splashScreen.setKeepOnScreenCondition { !dataReady } + super.onCreate(savedInstanceState) enableEdgeToEdge() + + // Initialize on main thread (these are fast SharedPreferences/DB reads) AppLanguage.init(this) IconLibraryRepository.init(this) ServerRepository.init(this) + dataReady = true + setContent { MainApp() } diff --git a/app/src/main/kotlin/com/example/myapplication/MainApp.kt b/app/src/main/kotlin/com/example/myapplication/MainApp.kt index 9c1d769..e1edc1c 100644 --- a/app/src/main/kotlin/com/example/myapplication/MainApp.kt +++ b/app/src/main/kotlin/com/example/myapplication/MainApp.kt @@ -1,4 +1,4 @@ -package com.example.myapplication +package com.example.myapplication import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi @@ -96,7 +96,7 @@ fun MainApp() { val iconLibraries by IconLibraryRepository.libraries.collectAsState() val coroutineScope = rememberCoroutineScope() - val selectedServer = servers.find { it.id == selectedServerId } ?: servers.firstOrNull() + val selectedServer = servers.find { it.id == selectedServerId } LaunchedEffect(servers.size) { if (servers.isNotEmpty()) { @@ -109,14 +109,11 @@ fun MainApp() { showStartScreen = false } - if (showStartScreen) { - Box(Modifier.fillMaxSize().background(Color.White)) { - Image( - painter = painterResource(Res.drawable.app_start), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) + // Dismiss splash only after both: minimum display time AND correct server selected + LaunchedEffect(selectedServerId) { + if (selectedServerId.isNotEmpty()) { + delay(1200) + showStartScreen = false } } AppTheme.systemIsDark = androidx.compose.foundation.isSystemInDarkTheme() @@ -288,5 +285,4 @@ fun MainApp() { } } } - } } diff --git a/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt b/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt index 37a669d..6261aa3 100644 --- a/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt +++ b/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt @@ -38,6 +38,7 @@ data class EmbyServer( val lastActive: String = "", val iconUrl: String = "", val authError: String = "", + val isSelected: Boolean = false, ) { val fullUrl: String get() { @@ -72,4 +73,3 @@ data class EmbyServer( fun generateId(): String = "server_${UUID.randomUUID()}" } } - diff --git a/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt b/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt index fc976e2..2c28ee8 100644 --- a/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt +++ b/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt @@ -1,4 +1,4 @@ -package com.example.myapplication.data +package com.example.myapplication.data import android.content.Context import kotlinx.coroutines.CoroutineScope @@ -53,7 +53,6 @@ object ServerRepository { } fun setRuntimeIcon(serverId: String, iconUrl: String) { - // Update memory immediately for responsive UI... val updated = mutableListOf() var changed: EmbyServer? = null for (server in _servers.value) { @@ -65,7 +64,6 @@ object ServerRepository { } } _servers.value = updated - // ...and persist to Room so the icon survives app restarts changed?.let { srv -> repositoryScope.launch { database?.serverDao()?.insertServer(toEntity(srv)) @@ -82,16 +80,13 @@ object ServerRepository { suspend fun checkAllServersOnline() { val currentServers = _servers.value.toList() for (server in currentServers) { - // First verify server is reachable val reachable = EmbyRepository.checkOnline(server) if (!reachable) { updateServerStatus(server.id, false, "无法连接到服务器") continue } - // Then try to authenticate to verify credentials val authResult = EmbyRepository.authenticate(server) if (authResult.success) { - // Update token if it's different from stored one if (server.apiKey != authResult.accessToken) { val updated = server.copy( apiKey = authResult.accessToken, @@ -112,6 +107,14 @@ object ServerRepository { } } + suspend fun setSelectedServer(serverId: String) { + withContext(Dispatchers.IO) { + database?.serverDao()?.clearAllSelected() + database?.serverDao()?.setSelected(serverId) + } + reload() + } + private suspend fun reload(): List { val previousServers = _servers.value.toList() val entities = withContext(Dispatchers.IO) { @@ -119,7 +122,6 @@ object ServerRepository { } val result = mutableListOf() for (entity in entities) { - // Preserve runtime-only fields (isOnline, lastActive) from previous state val prev = previousServers.find { it.id == entity.id } result.add( toModel(entity).copy( @@ -161,12 +163,12 @@ object ServerRepository { } } - private suspend fun removeLegacySampleServers() { withContext(Dispatchers.IO) { database?.serverDao()?.deleteLegacySampleServers() } } + private fun toEntity(server: EmbyServer): ServerEntity { return ServerEntity( server.id, @@ -180,6 +182,7 @@ object ServerRepository { server.apiKey, server.userId, server.iconUrl, + if (server.isSelected) 1 else 0, ) } @@ -196,9 +199,7 @@ object ServerRepository { apiKey = entity.accessToken ?: "", userId = entity.userId ?: "", iconUrl = entity.iconUrl ?: "", + isSelected = entity.isSelected == 1, ) } -} - - - +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt index f360319..28b0d42 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt @@ -79,6 +79,25 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import top.yukonga.miuix.kmp.basic.Text import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.TextStyle +import com.example.myapplication.ui.AppTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import androidx.compose.runtime.mutableIntStateOf /* ---- Adaptive content colors for the immersive home background ---- */ val LocalHomeContentColor = staticCompositionLocalOf { Color.Unspecified } @@ -457,6 +476,8 @@ fun HomeScreen( servers: List = emptyList(), selected: EmbyServer? = null, onDetailVisibilityChanged: (Boolean) -> Unit = {}, + onSelectServer: (String) -> Unit = {}, + hazeState: HazeState? = null, ) { val activeServer = remember(servers, selected) { selected ?: servers.firstOrNull { it.apiKey.isNotBlank() } } @@ -476,8 +497,11 @@ fun HomeScreen( val activeHeroItem = heroItems.getOrNull(heroPagerState.currentPage) val immersiveBackground = rememberPosterImmersiveBackground(heroImageUrl(activeHeroItem, activeServer?.fullUrl.orEmpty())) - LaunchedEffect(activeServer?.id, activeServer?.apiKey) { + var lastLoadedServerId by remember { mutableStateOf("") } + + LaunchedEffect(activeServer?.id) { if (activeServer == null || activeServer.apiKey.isBlank()) return@LaunchedEffect + if (activeServer.id == lastLoadedServerId) return@LaunchedEffect loading = true try { val libsDeferred = async { EmbyRepository.getLibraries(activeServer) } diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a1bc890..74a9148 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -8,6 +8,7 @@