Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
944291df49
10 files changed
+110
-32
No files matched your search
+17
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EmbyServer>()
|
||||
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<EmbyServer> {
|
||||
val previousServers = _servers.value.toList()
|
||||
val entities = withContext(Dispatchers.IO) {
|
||||
@@ -119,7 +122,6 @@ object ServerRepository {
|
||||
}
|
||||
val result = mutableListOf<EmbyServer>()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<EmbyServer> = 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) }
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<style name="Theme.MyApplication.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/window_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
|
||||
<item name="android:windowBackground">@color/window_background</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.MyApplication</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in new issue
Block a user