feat: themed splash screen with status messages

This commit is contained in:
edde746
2026-02-26 11:55:45 +01:00
parent 4b6c82b071
commit 8eda892b68
48 changed files with 472 additions and 81 deletions
@@ -26,12 +26,36 @@ class MainActivity : FlutterActivity() {
private val PIP_CHANNEL = "app.plezy/pip"
private val EXTERNAL_PLAYER_CHANNEL = "app.plezy/external_player"
private val THEME_CHANNEL = "app.plezy/theme"
private var watchNextPlugin: WatchNextPlugin? = null
private var cachedFlutterView: android.view.View? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Apply persisted theme color to the window background before anything
// else renders. This prevents a white flash between the native splash
// screen and Flutter's first frame for non-default themes (e.g. OLED).
val prefs = getSharedPreferences("plezy_prefs", Context.MODE_PRIVATE)
val savedTheme = prefs.getString("splash_theme", null)
if (savedTheme != null) {
val color = when (savedTheme) {
"oled" -> android.graphics.Color.BLACK
"dark" -> android.graphics.Color.parseColor("#0E0F12")
"light" -> android.graphics.Color.parseColor("#F7F7F8")
else -> null
}
if (color != null) {
window.decorView.setBackgroundColor(color)
}
}
super.onCreate(savedInstanceState)
// Disable the Android splash screen fade-out animation to avoid
// a flicker before Flutter draws its first frame.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView -> splashScreenView.remove() }
}
// Disable Android's default focus highlight ring that appears when using
// D-pad navigation so the Flutter UI can render its own focus state.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -153,6 +177,41 @@ class MainActivity : FlutterActivity() {
}
}
// Splash screen theme: persist user's chosen theme for next launch (API 31+)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, THEME_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"setSplashTheme" -> {
val mode = call.argument<String>("mode")
// Persist for next cold start & update window background now
getSharedPreferences("plezy_prefs", Context.MODE_PRIVATE)
.edit().putString("splash_theme", mode).apply()
val color = when (mode) {
"oled" -> android.graphics.Color.BLACK
"dark" -> android.graphics.Color.parseColor("#0E0F12")
"light" -> android.graphics.Color.parseColor("#F7F7F8")
else -> null
}
if (color != null) {
window.decorView.setBackgroundColor(color)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val themeId = when (mode) {
"dark" -> R.style.SplashTheme_Dark
"oled" -> R.style.SplashTheme_Oled
"light" -> R.style.SplashTheme_Light
"system" -> android.content.res.Resources.ID_NULL
else -> android.content.res.Resources.ID_NULL
}
splashScreen.setSplashScreenTheme(themeId)
}
result.success(true)
}
else -> result.notImplemented()
}
}
// Register Watch Next plugin and keep reference for deep link handling
watchNextPlugin = WatchNextPlugin()
flutterEngine.plugins.add(watchNextPlugin!!)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<vector xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportHeight="911.63"
android:viewportWidth="911.63">
<group android:translateX="276.65" android:translateY="223.88">
<path android:pathData="M335.65,192.66L68.01,38.14C37.78,20.69,0,42.5,0,77.41v309.04c0,34.91,37.78,56.72,68.01,39.27l267.64-154.52c30.23-17.45,30.23-61.08,0-78.54ZM255.53,276.8c-19.1,17.66-38.75,26.49-58.4,26.49s-39.29-8.83-58.39-26.49c-14.39-13.3-28.55-20.05-42.11-20.05s-27.72,6.75-42.1,20.05c-4.87,4.5-12.46,4.2-16.96-0.67-4.5-4.86-4.2-12.45 0.67-16.95,38.2-35.33,78.59-35.33,116.79,0,14.38,13.3,28.55,20.04,42.1,20.04s27.72-6.74,42.11-20.04c4.86-4.5,12.46-4.21,16.95,0.66,4.5,4.87,4.21,12.46-0.66,16.96ZM255.53,204.04c-19.1,17.67-38.75,26.5-58.4,26.5s-39.29-8.83-58.39-26.5c-14.39-13.3-28.55-20.04-42.11-20.04s-27.72,6.74-42.1,20.04c-4.87,4.5-12.46,4.21-16.96-0.66s-4.2-12.46,0.67-16.96c38.2-35.32,78.59-35.32,116.79,0,14.38,13.3,28.55,20.05,42.1,20.05s27.72-6.75,42.11-20.05c4.86-4.5,12.46-4.2,16.95,0.67,4.5,4.86,4.21,12.45-0.66,16.95Z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="22.67"
android:startY="425.72"
android:endX="201.83"
android:endY="115.4">
<item android:color="#FFAB543A" android:offset="0.0" />
<item android:color="#FFFF7E57" android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
</group>
</vector>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportHeight="911.63"
android:viewportWidth="911.63">
<group android:translateX="276.65" android:translateY="223.88">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M335.65,192.66L68.01,38.14C37.78,20.69,0,42.5,0,77.41v309.04c0,34.91,37.78,56.72,68.01,39.27l267.64-154.52c30.23-17.45,30.23-61.08,0-78.54ZM255.53,276.8c-19.1,17.66-38.75,26.49-58.4,26.49s-39.29-8.83-58.39-26.49c-14.39-13.3-28.55-20.05-42.11-20.05s-27.72,6.75-42.1,20.05c-4.87,4.5-12.46,4.2-16.96-0.67-4.5-4.86-4.2-12.45 0.67-16.95,38.2-35.33,78.59-35.33,116.79,0,14.38,13.3,28.55,20.04,42.1,20.04s27.72-6.74,42.11-20.04c4.86-4.5,12.46-4.21,16.95,0.66,4.5,4.87,4.21,12.46-0.66,16.96ZM255.53,204.04c-19.1,17.67-38.75,26.5-58.4,26.5s-39.29-8.83-58.39-26.5c-14.39-13.3-28.55-20.04-42.11-20.04s-27.72,6.74-42.1,20.04c-4.87,4.5-12.46,4.21-16.96-0.66s-4.2-12.46,0.67-16.96c38.2-35.32,78.59-35.32,116.79,0,14.38,13.3,28.55,20.05,42.1,20.05s27.72-6.75,42.11-20.05c4.86-4.5,12.46-4.2,16.95,0.67,4.5,4.86,4.21,12.45-0.66,16.95Z" />
</group>
</vector>
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_launcher_foreground" />
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
<monochrome>
<inset
android:drawable="@mipmap/ic_launcher_monochrome"
android:inset="16%" />
</monochrome>
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Dark" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_dark</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Oled" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_oled</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Light" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_light</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.System" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
</resources>
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1a1a1a</color>
<color name="splash_background">#0E0F12</color>
<color name="splash_dark">#0E0F12</color>
<color name="splash_oled">#000000</color>
<color name="splash_light">#F7F7F8</color>
</resources>
@@ -2,6 +2,7 @@
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<!-- Disable Android's default green focus highlight that appears during d-pad/keyboard navigation -->
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
@@ -13,9 +14,31 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item>
<item name="android:windowBackground">@color/splash_background</item>
<!-- Disable Android's default green focus highlight that appears during d-pad/keyboard navigation -->
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<!-- Splash theme variants for setSplashScreenTheme -->
<style name="SplashTheme.Dark" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_dark</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Oled" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_oled</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Light" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_light</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.System" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
</resources>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Dark" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_dark</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Oled" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_oled</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Light" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_light</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.System" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
</resources>
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#ffffff</color>
<color name="splash_background">#F7F7F8</color>
<color name="splash_dark">#0E0F12</color>
<color name="splash_oled">#000000</color>
<color name="splash_light">#F7F7F8</color>
</resources>
+24 -1
View File
@@ -2,6 +2,7 @@
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<!-- Disable Android's default green focus highlight that appears during d-pad/keyboard navigation -->
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
@@ -13,9 +14,31 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@android:color/black</item>
<item name="android:windowBackground">@color/splash_background</item>
<!-- Disable Android's default green focus highlight that appears during d-pad/keyboard navigation -->
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<!-- Splash theme variants for setSplashScreenTheme -->
<style name="SplashTheme.Dark" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_dark</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Oled" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_oled</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.Light" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_light</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
<style name="SplashTheme.System" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:defaultFocusHighlightEnabled">false</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
</resources>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-276.65 -223.88 911.63 911.63"><defs><linearGradient id="linear-gradient" x1="22.67" y1="425.72" x2="201.83" y2="115.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ab543a"/><stop offset="1" stop-color="#ff7e57"/></linearGradient></defs><path fill="url(#linear-gradient)" d="M335.65,192.66L68.01,38.14C37.78,20.69,0,42.5,0,77.41v309.04c0,34.91,37.78,56.72,68.01,39.27l267.64-154.52c30.23-17.45,30.23-61.08,0-78.54ZM255.53,276.8c-19.1,17.66-38.75,26.49-58.4,26.49s-39.29-8.83-58.39-26.49c-14.39-13.3-28.55-20.05-42.11-20.05s-27.72,6.75-42.1,20.05c-4.87,4.5-12.46,4.2-16.96-.67-4.5-4.86-4.2-12.45.67-16.95,38.2-35.33,78.59-35.33,116.79,0,14.38,13.3,28.55,20.04,42.1,20.04s27.72-6.74,42.11-20.04c4.86-4.5,12.46-4.21,16.95.66,4.5,4.87,4.21,12.46-.66,16.96ZM255.53,204.04c-19.1,17.67-38.75,26.5-58.4,26.5s-39.29-8.83-58.39-26.5c-14.39-13.3-28.55-20.04-42.11-20.04s-27.72,6.74-42.1,20.04c-4.87,4.5-12.46,4.21-16.96-.66s-4.2-12.46.67-16.96c38.2-35.32,78.59-35.32,116.79,0,14.38,13.3,28.55,20.05,42.1,20.05s27.72-6.75,42.11-20.05c4.86-4.5,12.46-4.2,16.95.67,4.5,4.86,4.21,12.45-.66,16.95Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Möchtest du die App wirklich beenden?",
"dontAskAgain": "Nicht erneut fragen",
"exit": "Beenden",
"viewAll": "Alle anzeigen"
"viewAll": "Alle anzeigen",
"checkingNetwork": "Netzwerk wird geprüft...",
"refreshingServers": "Server werden aktualisiert...",
"loadingServers": "Server werden geladen...",
"connectingToServers": "Verbindung zu Servern...",
"startingOfflineMode": "Offlinemodus wird gestartet..."
},
"screens": {
"licenses": "Lizenzen",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Are you sure you want to exit?",
"dontAskAgain": "Don't ask again",
"exit": "Exit",
"viewAll": "View All"
"viewAll": "View All",
"checkingNetwork": "Checking network...",
"refreshingServers": "Refreshing servers...",
"loadingServers": "Loading servers...",
"connectingToServers": "Connecting to servers...",
"startingOfflineMode": "Starting offline mode..."
},
"screens": {
"licenses": "Licenses",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "¿Estás seguro de que quieres salir?",
"dontAskAgain": "No volver a preguntar",
"exit": "Salir",
"viewAll": "Ver todo"
"viewAll": "Ver todo",
"checkingNetwork": "Comprobando red...",
"refreshingServers": "Actualizando servidores...",
"loadingServers": "Cargando servidores...",
"connectingToServers": "Conectando a servidores...",
"startingOfflineMode": "Iniciando modo sin conexión..."
},
"screens": {
"licenses": "Licencias",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Êtes-vous sûr de vouloir quitter ?",
"dontAskAgain": "Ne plus demander",
"exit": "Quitter",
"viewAll": "Tout afficher"
"viewAll": "Tout afficher",
"checkingNetwork": "Vérification du réseau...",
"refreshingServers": "Actualisation des serveurs...",
"loadingServers": "Chargement des serveurs...",
"connectingToServers": "Connexion aux serveurs...",
"startingOfflineMode": "Démarrage en mode hors-ligne..."
},
"screens": {
"licenses": "Licenses",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Sei sicuro di voler uscire?",
"dontAskAgain": "Non chiedere più",
"exit": "Esci",
"viewAll": "Mostra tutto"
"viewAll": "Mostra tutto",
"checkingNetwork": "Verifica rete...",
"refreshingServers": "Aggiornamento server...",
"loadingServers": "Caricamento server...",
"connectingToServers": "Connessione ai server...",
"startingOfflineMode": "Avvio modalità offline..."
},
"screens": {
"licenses": "Licenze",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "정말 종료하시겠습니까?",
"dontAskAgain": "다시 묻지 않기",
"exit": "종료",
"viewAll": "모두 보기"
"viewAll": "모두 보기",
"checkingNetwork": "네트워크 확인 중...",
"refreshingServers": "서버 새로고침 중...",
"loadingServers": "서버 로딩 중...",
"connectingToServers": "서버 연결 중...",
"startingOfflineMode": "오프라인 모드 시작 중..."
},
"screens": {
"licenses": "라이선스",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Weet je zeker dat je wilt afsluiten?",
"dontAskAgain": "Niet meer vragen",
"exit": "Afsluiten",
"viewAll": "Alles weergeven"
"viewAll": "Alles weergeven",
"checkingNetwork": "Netwerk controleren...",
"refreshingServers": "Servers vernieuwen...",
"loadingServers": "Servers laden...",
"connectingToServers": "Verbinden met servers...",
"startingOfflineMode": "Offlinemodus starten..."
},
"screens": {
"licenses": "Licenties",
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonDe implements TranslationsCommonEn {
@override String get dontAskAgain => 'Nicht erneut fragen';
@override String get exit => 'Beenden';
@override String get viewAll => 'Alle anzeigen';
@override String get checkingNetwork => 'Netzwerk wird geprüft...';
@override String get refreshingServers => 'Server werden aktualisiert...';
@override String get loadingServers => 'Server werden geladen...';
@override String get connectingToServers => 'Verbindung zu Servern...';
@override String get startingOfflineMode => 'Offlinemodus wird gestartet...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsDe {
'common.dontAskAgain' => 'Nicht erneut fragen',
'common.exit' => 'Beenden',
'common.viewAll' => 'Alle anzeigen',
'common.checkingNetwork' => 'Netzwerk wird geprüft...',
'common.refreshingServers' => 'Server werden aktualisiert...',
'common.loadingServers' => 'Server werden geladen...',
'common.connectingToServers' => 'Verbindung zu Servern...',
'common.startingOfflineMode' => 'Offlinemodus wird gestartet...',
'screens.licenses' => 'Lizenzen',
'screens.switchProfile' => 'Profil wechseln',
'screens.subtitleStyling' => 'Untertitel-Stil',
+20
View File
@@ -252,6 +252,21 @@ class TranslationsCommonEn {
/// en: 'View All'
String get viewAll => 'View All';
/// en: 'Checking network...'
String get checkingNetwork => 'Checking network...';
/// en: 'Refreshing servers...'
String get refreshingServers => 'Refreshing servers...';
/// en: 'Loading servers...'
String get loadingServers => 'Loading servers...';
/// en: 'Connecting to servers...'
String get connectingToServers => 'Connecting to servers...';
/// en: 'Starting offline mode...'
String get startingOfflineMode => 'Starting offline mode...';
}
// Path: screens
@@ -2930,6 +2945,11 @@ extension on Translations {
'common.dontAskAgain' => 'Don\'t ask again',
'common.exit' => 'Exit',
'common.viewAll' => 'View All',
'common.checkingNetwork' => 'Checking network...',
'common.refreshingServers' => 'Refreshing servers...',
'common.loadingServers' => 'Loading servers...',
'common.connectingToServers' => 'Connecting to servers...',
'common.startingOfflineMode' => 'Starting offline mode...',
'screens.licenses' => 'Licenses',
'screens.switchProfile' => 'Switch Profile',
'screens.subtitleStyling' => 'Subtitle Styling',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonEs implements TranslationsCommonEn {
@override String get dontAskAgain => 'No volver a preguntar';
@override String get exit => 'Salir';
@override String get viewAll => 'Ver todo';
@override String get checkingNetwork => 'Comprobando red...';
@override String get refreshingServers => 'Actualizando servidores...';
@override String get loadingServers => 'Cargando servidores...';
@override String get connectingToServers => 'Conectando a servidores...';
@override String get startingOfflineMode => 'Iniciando modo sin conexión...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsEs {
'common.dontAskAgain' => 'No volver a preguntar',
'common.exit' => 'Salir',
'common.viewAll' => 'Ver todo',
'common.checkingNetwork' => 'Comprobando red...',
'common.refreshingServers' => 'Actualizando servidores...',
'common.loadingServers' => 'Cargando servidores...',
'common.connectingToServers' => 'Conectando a servidores...',
'common.startingOfflineMode' => 'Iniciando modo sin conexión...',
'screens.licenses' => 'Licencias',
'screens.switchProfile' => 'Cambiar Perfil',
'screens.subtitleStyling' => 'Estilo de Subtítulos',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonFr implements TranslationsCommonEn {
@override String get dontAskAgain => 'Ne plus demander';
@override String get exit => 'Quitter';
@override String get viewAll => 'Tout afficher';
@override String get checkingNetwork => 'Vérification du réseau...';
@override String get refreshingServers => 'Actualisation des serveurs...';
@override String get loadingServers => 'Chargement des serveurs...';
@override String get connectingToServers => 'Connexion aux serveurs...';
@override String get startingOfflineMode => 'Démarrage en mode hors-ligne...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsFr {
'common.dontAskAgain' => 'Ne plus demander',
'common.exit' => 'Quitter',
'common.viewAll' => 'Tout afficher',
'common.checkingNetwork' => 'Vérification du réseau...',
'common.refreshingServers' => 'Actualisation des serveurs...',
'common.loadingServers' => 'Chargement des serveurs...',
'common.connectingToServers' => 'Connexion aux serveurs...',
'common.startingOfflineMode' => 'Démarrage en mode hors-ligne...',
'screens.licenses' => 'Licenses',
'screens.switchProfile' => 'Changer de profil',
'screens.subtitleStyling' => 'Configuration des sous-titres',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonIt implements TranslationsCommonEn {
@override String get dontAskAgain => 'Non chiedere più';
@override String get exit => 'Esci';
@override String get viewAll => 'Mostra tutto';
@override String get checkingNetwork => 'Verifica rete...';
@override String get refreshingServers => 'Aggiornamento server...';
@override String get loadingServers => 'Caricamento server...';
@override String get connectingToServers => 'Connessione ai server...';
@override String get startingOfflineMode => 'Avvio modalità offline...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsIt {
'common.dontAskAgain' => 'Non chiedere più',
'common.exit' => 'Esci',
'common.viewAll' => 'Mostra tutto',
'common.checkingNetwork' => 'Verifica rete...',
'common.refreshingServers' => 'Aggiornamento server...',
'common.loadingServers' => 'Caricamento server...',
'common.connectingToServers' => 'Connessione ai server...',
'common.startingOfflineMode' => 'Avvio modalità offline...',
'screens.licenses' => 'Licenze',
'screens.switchProfile' => 'Cambia profilo',
'screens.subtitleStyling' => 'Stile sottotitoli',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonKo implements TranslationsCommonEn {
@override String get dontAskAgain => '다시 묻지 않기';
@override String get exit => '종료';
@override String get viewAll => '모두 보기';
@override String get checkingNetwork => '네트워크 확인 중...';
@override String get refreshingServers => '서버 새로고침 중...';
@override String get loadingServers => '서버 로딩 중...';
@override String get connectingToServers => '서버 연결 중...';
@override String get startingOfflineMode => '오프라인 모드 시작 중...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsKo {
'common.dontAskAgain' => '다시 묻지 않기',
'common.exit' => '종료',
'common.viewAll' => '모두 보기',
'common.checkingNetwork' => '네트워크 확인 중...',
'common.refreshingServers' => '서버 새로고침 중...',
'common.loadingServers' => '서버 로딩 중...',
'common.connectingToServers' => '서버 연결 중...',
'common.startingOfflineMode' => '오프라인 모드 시작 중...',
'screens.licenses' => '라이선스',
'screens.switchProfile' => '프로필 전환',
'screens.subtitleStyling' => '자막 스타일 설정',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonNl implements TranslationsCommonEn {
@override String get dontAskAgain => 'Niet meer vragen';
@override String get exit => 'Afsluiten';
@override String get viewAll => 'Alles weergeven';
@override String get checkingNetwork => 'Netwerk controleren...';
@override String get refreshingServers => 'Servers vernieuwen...';
@override String get loadingServers => 'Servers laden...';
@override String get connectingToServers => 'Verbinden met servers...';
@override String get startingOfflineMode => 'Offlinemodus starten...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsNl {
'common.dontAskAgain' => 'Niet meer vragen',
'common.exit' => 'Afsluiten',
'common.viewAll' => 'Alles weergeven',
'common.checkingNetwork' => 'Netwerk controleren...',
'common.refreshingServers' => 'Servers vernieuwen...',
'common.loadingServers' => 'Servers laden...',
'common.connectingToServers' => 'Verbinden met servers...',
'common.startingOfflineMode' => 'Offlinemodus starten...',
'screens.licenses' => 'Licenties',
'screens.switchProfile' => 'Wissel van profiel',
'screens.subtitleStyling' => 'Ondertitel opmaak',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonSv implements TranslationsCommonEn {
@override String get dontAskAgain => 'Fråga inte igen';
@override String get exit => 'Avsluta';
@override String get viewAll => 'Visa alla';
@override String get checkingNetwork => 'Kontrollerar nätverk...';
@override String get refreshingServers => 'Uppdaterar servrar...';
@override String get loadingServers => 'Laddar servrar...';
@override String get connectingToServers => 'Ansluter till servrar...';
@override String get startingOfflineMode => 'Startar offlineläge...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsSv {
'common.dontAskAgain' => 'Fråga inte igen',
'common.exit' => 'Avsluta',
'common.viewAll' => 'Visa alla',
'common.checkingNetwork' => 'Kontrollerar nätverk...',
'common.refreshingServers' => 'Uppdaterar servrar...',
'common.loadingServers' => 'Laddar servrar...',
'common.connectingToServers' => 'Ansluter till servrar...',
'common.startingOfflineMode' => 'Startar offlineläge...',
'screens.licenses' => 'Licenser',
'screens.switchProfile' => 'Byt profil',
'screens.subtitleStyling' => 'Undertext-styling',
+10
View File
@@ -151,6 +151,11 @@ class _TranslationsCommonZh implements TranslationsCommonEn {
@override String get dontAskAgain => '不再询问';
@override String get exit => '退出';
@override String get viewAll => '查看全部';
@override String get checkingNetwork => '正在检查网络...';
@override String get refreshingServers => '正在刷新服务器...';
@override String get loadingServers => '正在加载服务器...';
@override String get connectingToServers => '正在连接服务器...';
@override String get startingOfflineMode => '正在启动离线模式...';
}
// Path: screens
@@ -1338,6 +1343,11 @@ extension on TranslationsZh {
'common.dontAskAgain' => '不再询问',
'common.exit' => '退出',
'common.viewAll' => '查看全部',
'common.checkingNetwork' => '正在检查网络...',
'common.refreshingServers' => '正在刷新服务器...',
'common.loadingServers' => '正在加载服务器...',
'common.connectingToServers' => '正在连接服务器...',
'common.startingOfflineMode' => '正在启动离线模式...',
'screens.licenses' => '许可证',
'screens.switchProfile' => '切换用户',
'screens.subtitleStyling' => '字幕样式',
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "Är du säker på att du vill avsluta?",
"dontAskAgain": "Fråga inte igen",
"exit": "Avsluta",
"viewAll": "Visa alla"
"viewAll": "Visa alla",
"checkingNetwork": "Kontrollerar nätverk...",
"refreshingServers": "Uppdaterar servrar...",
"loadingServers": "Laddar servrar...",
"connectingToServers": "Ansluter till servrar...",
"startingOfflineMode": "Startar offlineläge..."
},
"screens": {
"licenses": "Licenser",
+6 -1
View File
@@ -52,7 +52,12 @@
"exitConfirmMessage": "确定要退出吗?",
"dontAskAgain": "不再询问",
"exit": "退出",
"viewAll": "查看全部"
"viewAll": "查看全部",
"checkingNetwork": "正在检查网络...",
"refreshingServers": "正在刷新服务器...",
"loadingServers": "正在加载服务器...",
"connectingToServers": "正在连接服务器...",
"startingOfflineMode": "正在启动离线模式..."
},
"screens": {
"licenses": "许可证",
+52 -12
View File
@@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
import 'dart:io' show Platform;
import 'package:window_manager/window_manager.dart';
import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'screens/main_screen.dart';
import 'screens/auth_screen.dart';
import 'services/storage_service.dart';
@@ -46,6 +47,7 @@ import 'i18n/strings.g.dart';
import 'focus/input_mode_tracker.dart';
import 'focus/key_event_utils.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'utils/navigation_transitions.dart';
// Workaround for Flutter bug #177992: iPadOS 26.1+ misinterprets fake touch events
// at (0,0) as barrier taps, causing modals to dismiss immediately.
@@ -368,13 +370,21 @@ class SetupScreen extends StatefulWidget {
}
class _SetupScreenState extends State<SetupScreen> {
String _statusMessage = '';
@override
void initState() {
super.initState();
_loadSavedCredentials();
}
void _setStatus(String message) {
if (mounted) setState(() => _statusMessage = message);
}
Future<void> _loadSavedCredentials() async {
_setStatus(t.common.checkingNetwork);
final storage = await StorageService.getInstance();
final registry = ServerRegistry(storage);
@@ -386,6 +396,8 @@ class _SetupScreenState extends State<SetupScreen> {
final hasNetwork = !connectivityResult.contains(ConnectivityResult.none);
if (hasNetwork) {
_setStatus(t.common.refreshingServers);
// Refresh servers from API to get updated connection info (IPs may change).
// If the stored token is invalid (e.g. after removing a Plex profile PIN),
// redirect to AuthScreen so the user can re-authenticate.
@@ -393,18 +405,20 @@ class _SetupScreenState extends State<SetupScreen> {
if (refreshResult == ServerRefreshResult.authError) {
await storage.clearCredentials();
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const AuthScreen()));
Navigator.pushReplacement(context, fadeRoute(const AuthScreen()));
}
return;
}
}
_setStatus(t.common.loadingServers);
// Load all configured servers
final servers = await registry.getServers();
if (servers.isEmpty) {
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const AuthScreen()));
Navigator.pushReplacement(context, fadeRoute(const AuthScreen()));
}
return;
}
@@ -413,15 +427,18 @@ class _SetupScreenState extends State<SetupScreen> {
// No network — skip connection attempts and go straight to offline mode
if (!hasNetwork) {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainScreen(isOfflineMode: true)),
fadeRoute(const MainScreen(isOfflineMode: true)),
);
return;
}
_setStatus(t.common.connectingToServers);
try {
final result = await ServerConnectionOrchestrator.connectAndInitialize(
servers: servers,
@@ -442,25 +459,27 @@ class _SetupScreenState extends State<SetupScreen> {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainScreen(client: result.firstClient!)),
fadeRoute(MainScreen(client: result.firstClient!)),
);
} else {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainScreen(isOfflineMode: true)),
fadeRoute(const MainScreen(isOfflineMode: true)),
);
}
} catch (e, stackTrace) {
appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace);
if (mounted) {
_setStatus(t.common.startingOfflineMode);
await context.read<DownloadProvider>().ensureInitialized();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainScreen(isOfflineMode: true)),
fadeRoute(const MainScreen(isOfflineMode: true)),
);
}
}
@@ -468,12 +487,33 @@ class _SetupScreenState extends State<SetupScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [const CircularProgressIndicator(), const SizedBox(height: 16), Text(t.common.loading)],
),
return ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
child: Stack(
children: [
// Icon dead-center, matching Android 12+ splash position.
// 192dp accounts for the 16% inset in ic_launcher.xml.
Center(
child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288),
),
// Status text below center, independent of icon position.
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height * 0.5 - 140,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_statusMessage,
key: ValueKey(_statusMessage),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
),
),
],
),
);
}
+17
View File
@@ -1,4 +1,6 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../services/settings_service.dart' as settings;
import '../theme/mono_theme.dart';
@@ -24,6 +26,7 @@ class ThemeProvider extends ChangeNotifier {
Future<void> _initializeSettings() async {
_settingsService = await settings.SettingsService.getInstance();
_themeMode = _settingsService.getThemeMode();
_updateSplashTheme(_themeMode);
notifyListeners();
}
@@ -63,14 +66,28 @@ class ThemeProvider extends ChangeNotifier {
}
}
static const _themeChannel = MethodChannel('app.plezy/theme');
Future<void> setThemeMode(settings.ThemeMode mode) async {
if (_themeMode != mode) {
_themeMode = mode;
await _settingsService.setThemeMode(mode);
_updateSplashTheme(mode);
notifyListeners();
}
}
void _updateSplashTheme(settings.ThemeMode mode) {
if (!Platform.isAndroid) return;
final name = switch (mode) {
settings.ThemeMode.dark => 'dark',
settings.ThemeMode.oled => 'oled',
settings.ThemeMode.light => 'light',
settings.ThemeMode.system => 'system',
};
_themeChannel.invokeMethod('setSplashTheme', {'mode': name});
}
String get themeModeDisplayName {
switch (_themeMode) {
case settings.ThemeMode.light:
+2 -1
View File
@@ -17,6 +17,7 @@ import '../theme/mono_tokens.dart';
import '../utils/app_logger.dart';
import '../utils/platform_detector.dart';
import '../focus/focusable_button.dart';
import '../utils/navigation_transitions.dart';
import 'main_screen.dart';
class AuthScreen extends StatefulWidget {
@@ -114,7 +115,7 @@ class _AuthScreenState extends State<AuthScreen> {
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainScreen(client: result.firstClient!)),
fadeRoute(MainScreen(client: result.firstClient!)),
);
} catch (e) {
appLogger.e('Failed to connect to servers', error: e);
+12
View File
@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
Route<T> fadeRoute<T>(Widget page) {
return PageRouteBuilder<T>(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: const Duration(milliseconds: 500),
reverseTransitionDuration: const Duration(milliseconds: 500),
);
}
-32
View File
@@ -41,14 +41,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args:
dependency: transitive
description:
@@ -488,14 +480,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -591,14 +575,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
in_app_review:
dependency: "direct main"
description:
@@ -896,14 +872,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
process:
dependency: transitive
description:
+1 -15
View File
@@ -65,7 +65,6 @@ dev_dependencies:
flutter_lints: ^6.0.0
build_runner: ^2.4.7
json_serializable: ^6.7.1
flutter_launcher_icons: ^0.14.4
slang_build_runner: ^4.12.0
dart_code_linter: ^3.2.1
drift_dev: ^2.14.0
@@ -81,22 +80,9 @@ flutter:
assets:
- lib/data/iso_639_codes.json
- assets/plezy.png
- assets/plezy_adaptive_foreground.svg
- assets/go-noto-current-regular.ttf
- assets/shaders/nvscaler/
- assets/shaders/anime4k/
- assets/player_icons/
- assets/rating_icons/
flutter_launcher_icons:
android: true
ios: false
image_path: "assets/plezy.png"
adaptive_icon_background: "#ffffff"
adaptive_icon_foreground: "assets/plezy_android_foreground.png"
adaptive_icon_monochrome: "assets/plezy_monochrome.png"
macos:
generate: false
windows:
generate: true
linux:
generate: true