add config icon feature
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.lyric
|
||||
|
||||
actual object PlatformInfo {
|
||||
actual val allowEulaLyric: Boolean = true
|
||||
actual val extendedFeaturesEnabled: Boolean = true
|
||||
actual val isIos: Boolean = false
|
||||
actual val appVersion: String = ""
|
||||
}
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Generated by scripts/export_app_icons.py. Do not edit.
|
||||
package com.zhushenwudi.lovelivemusicplayer.appicon
|
||||
|
||||
data class AppIconCharacter(val id: String, val name: String, val groupId: String)
|
||||
data class AppIconGroup(val id: String, val name: String)
|
||||
|
||||
object AppIconCatalog {
|
||||
const val DEFAULT = "default"
|
||||
val groups = listOf(
|
||||
AppIconGroup("muse", "μ’s"),
|
||||
AppIconGroup("a-rise", "A-RISE"),
|
||||
AppIconGroup("aqours", "Aqours"),
|
||||
AppIconGroup("saint-snow", "Saint Snow"),
|
||||
AppIconGroup("nijigasaki", "虹ヶ咲学園スクールアイドル同好会"),
|
||||
AppIconGroup("liella", "Liella!"),
|
||||
AppIconGroup("sunny-passion", "Sunny Passion"),
|
||||
AppIconGroup("hasunosora", "蓮ノ空女学院スクールアイドルクラブ"),
|
||||
AppIconGroup("ikizulive", "イキヅライブ!/いきづらい部!"),
|
||||
AppIconGroup("school-idol-musical", "スクールアイドルミュージカル"),
|
||||
)
|
||||
val characters = listOf(
|
||||
AppIconCharacter("kosaka-honoka", "高坂穂乃果", "muse"),
|
||||
AppIconCharacter("ayase-eli", "絢瀬絵里", "muse"),
|
||||
AppIconCharacter("minami-kotori", "南小鸟", "muse"),
|
||||
AppIconCharacter("sonoda-umi", "園田海未", "muse"),
|
||||
AppIconCharacter("hoshizora-rin", "星空凛", "muse"),
|
||||
AppIconCharacter("nishikino-maki", "西木野真姫", "muse"),
|
||||
AppIconCharacter("tojo-nozomi", "東條希", "muse"),
|
||||
AppIconCharacter("koizumi-hanayo", "小泉花陽", "muse"),
|
||||
AppIconCharacter("yazawa-nico", "矢泽妮可", "muse"),
|
||||
AppIconCharacter("kira-tsubasa", "綺羅ツバサ", "a-rise"),
|
||||
AppIconCharacter("todo-erena", "統堂英玲奈", "a-rise"),
|
||||
AppIconCharacter("yuki-anju", "優木あんじゅ", "a-rise"),
|
||||
AppIconCharacter("takami-chika", "高海千歌", "aqours"),
|
||||
AppIconCharacter("sakurauchi-riko", "桜内梨子", "aqours"),
|
||||
AppIconCharacter("matsuura-kanan", "松浦果南", "aqours"),
|
||||
AppIconCharacter("kurosawa-dia", "黒澤ダイヤ", "aqours"),
|
||||
AppIconCharacter("watanabe-you", "渡辺曜", "aqours"),
|
||||
AppIconCharacter("tsushima-yoshiko", "津島善子", "aqours"),
|
||||
AppIconCharacter("kunikida-hanamaru", "国木田花丸", "aqours"),
|
||||
AppIconCharacter("ohara-mari", "小原鞠莉", "aqours"),
|
||||
AppIconCharacter("kurosawa-ruby", "黒澤ルビィ", "aqours"),
|
||||
AppIconCharacter("kazuno-sarah", "鹿角聖良", "saint-snow"),
|
||||
AppIconCharacter("kazuno-leah", "鹿角理亞", "saint-snow"),
|
||||
AppIconCharacter("takasaki-yu", "高咲侑", "nijigasaki"),
|
||||
AppIconCharacter("uehara-ayumu", "上原歩夢", "nijigasaki"),
|
||||
AppIconCharacter("nakasu-kasumi", "中須かすみ", "nijigasaki"),
|
||||
AppIconCharacter("osaka-shizuku", "桜坂しずく", "nijigasaki"),
|
||||
AppIconCharacter("asaka-karin", "朝香果林", "nijigasaki"),
|
||||
AppIconCharacter("miyashita-ai", "宮下愛", "nijigasaki"),
|
||||
AppIconCharacter("konoe-kanata", "近江彼方", "nijigasaki"),
|
||||
AppIconCharacter("yuki-setsuna", "優木せつ菜", "nijigasaki"),
|
||||
AppIconCharacter("verde-emma", "エマ・ヴェルデ", "nijigasaki"),
|
||||
AppIconCharacter("tennoji-rina", "天王寺璃奈", "nijigasaki"),
|
||||
AppIconCharacter("mifune-shioriko", "三船栞子", "nijigasaki"),
|
||||
AppIconCharacter("taylor-mia", "ミア・テイラー", "nijigasaki"),
|
||||
AppIconCharacter("zhong-lanzhu", "鐘嵐珠", "nijigasaki"),
|
||||
AppIconCharacter("shibuya-kanon", "澁谷かのん", "liella"),
|
||||
AppIconCharacter("tang-keke", "唐可可", "liella"),
|
||||
AppIconCharacter("arashi-chisato", "嵐千砂都", "liella"),
|
||||
AppIconCharacter("heanna-sumire", "平安名すみれ", "liella"),
|
||||
AppIconCharacter("hazuki-ren", "葉月恋", "liella"),
|
||||
AppIconCharacter("sakurakoji-kinako", "桜小路きな子", "liella"),
|
||||
AppIconCharacter("yoneme-mei", "米女メイ", "liella"),
|
||||
AppIconCharacter("wakana-shiki", "若菜四季", "liella"),
|
||||
AppIconCharacter("onitsuka-natsumi", "鬼塚夏美", "liella"),
|
||||
AppIconCharacter("wien-margarete", "ウィーン・マルガレーテ", "liella"),
|
||||
AppIconCharacter("onitsuka-tomari", "鬼塚冬毬", "liella"),
|
||||
AppIconCharacter("hiiragi-mao", "柊摩央", "sunny-passion"),
|
||||
AppIconCharacter("hijirisawa-yuna", "聖澤悠奈", "sunny-passion"),
|
||||
AppIconCharacter("hinoshita-kaho", "日野下花帆", "hasunosora"),
|
||||
AppIconCharacter("murano-sayaka", "村野さやか", "hasunosora"),
|
||||
AppIconCharacter("otomune-kozue", "乙宗梢", "hasunosora"),
|
||||
AppIconCharacter("yugiri-tsuzuri", "夕霧綴理", "hasunosora"),
|
||||
AppIconCharacter("osawa-rurino", "大沢瑠璃乃", "hasunosora"),
|
||||
AppIconCharacter("fujishima-megumi", "藤島慈", "hasunosora"),
|
||||
AppIconCharacter("momose-ginko", "百生吟子", "hasunosora"),
|
||||
AppIconCharacter("kachimachi-kosuzu", "徒町小鈴", "hasunosora"),
|
||||
AppIconCharacter("anyoji-hime", "安養寺姫芽", "hasunosora"),
|
||||
AppIconCharacter("ceras-yanagida-lilienfeld", "セラス・柳田・リリエンフェルト", "hasunosora"),
|
||||
AppIconCharacter("katsuragi-izumi", "桂城泉", "hasunosora"),
|
||||
AppIconCharacter("kinjo-maika", "錦上マイカ", "hasunosora"),
|
||||
AppIconCharacter("reisawa-aoi", "令沢葵", "hasunosora"),
|
||||
AppIconCharacter("shinowa-mion", "紫輪みおん", "hasunosora"),
|
||||
AppIconCharacter("takahashi-polka", "高橋ポルカ", "ikizulive"),
|
||||
AppIconCharacter("azabu-mai", "麻布麻衣", "ikizulive"),
|
||||
AppIconCharacter("goto-akira", "五桐玲", "ikizulive"),
|
||||
AppIconCharacter("komagata-hanabi", "駒形花火", "ikizulive"),
|
||||
AppIconCharacter("kanazawa-miracle", "金澤奇跡", "ikizulive"),
|
||||
AppIconCharacter("chofu-noriko", "調布のりこ", "ikizulive"),
|
||||
AppIconCharacter("harumiya-yukuri", "春宮ゆくり", "ikizulive"),
|
||||
AppIconCharacter("konohana-aurora", "此花輝夜", "ikizulive"),
|
||||
AppIconCharacter("yamada-midori", "山田真緑", "ikizulive"),
|
||||
AppIconCharacter("sasaki-shion", "佐々木翔音", "ikizulive"),
|
||||
AppIconCharacter("tsubaki-rurika", "椿ルリカ", "school-idol-musical"),
|
||||
AppIconCharacter("sumeragi-yuzuha", "皇ユズハ", "school-idol-musical"),
|
||||
AppIconCharacter("hojo-yukino", "北条ユキノ", "school-idol-musical"),
|
||||
AppIconCharacter("amakusa-hikaru", "天草ヒカル", "school-idol-musical"),
|
||||
AppIconCharacter("mikasa-maya", "三笠マーヤ", "school-idol-musical"),
|
||||
AppIconCharacter("takizawa-anzu", "滝沢アンズ", "school-idol-musical"),
|
||||
AppIconCharacter("wakatsuki-misuzu", "若槻ミスズ", "school-idol-musical"),
|
||||
AppIconCharacter("kurusu-toa", "来栖トア", "school-idol-musical"),
|
||||
AppIconCharacter("suzuka-rena", "鈴賀レナ", "school-idol-musical"),
|
||||
AppIconCharacter("harukaze-sayaka", "晴風サヤカ", "school-idol-musical"),
|
||||
)
|
||||
fun isValid(id: String) = id == DEFAULT || characters.any { it.id == id }
|
||||
fun iosName(id: String): String? = if (id == DEFAULT) null else "LLMP_" + id.replace('-', '_')
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.appicon
|
||||
|
||||
import com.zhushenwudi.lovelivemusicplayer.lyric.PlatformInfo
|
||||
import com.zhushenwudi.lovelivemusicplayer.settings.AppSettings
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
interface AppIconController {
|
||||
val supported: Boolean
|
||||
/** Localized explanation of the platform's appearance behavior. */
|
||||
val appearanceHintKey: String
|
||||
/** null means the platform has no single recognized active icon. */
|
||||
suspend fun currentCharacterId(): String?
|
||||
/** Publish committed previews for native widgets/branding; null bytes restore the default. */
|
||||
suspend fun publishArtwork(characterId: String, light: ByteArray?, dark: ByteArray?) {}
|
||||
suspend fun apply(characterId: String, systemDark: Boolean)
|
||||
}
|
||||
|
||||
object UnsupportedAppIconController : AppIconController {
|
||||
override val supported = false
|
||||
override val appearanceHintKey = "app_icon_unavailable"
|
||||
override suspend fun currentCharacterId(): String? = null
|
||||
override suspend fun apply(characterId: String, systemDark: Boolean) {
|
||||
error("App icons are unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
data class AppIconState(
|
||||
val characterId: String = AppIconCatalog.DEFAULT,
|
||||
val switching: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
/** Serializes native operations and commits preferences only after native success. */
|
||||
class AppIconManager(private val settings: AppSettings, val controller: AppIconController) {
|
||||
private val mutex = Mutex()
|
||||
private val mutableState = MutableStateFlow(AppIconState(
|
||||
characterId = settings.appIconCharacterId.takeIf(AppIconCatalog::isValid) ?: AppIconCatalog.DEFAULT,
|
||||
))
|
||||
val state = mutableState.asStateFlow()
|
||||
|
||||
val available: Boolean get() = PlatformInfo.extendedFeaturesEnabled && controller.supported
|
||||
|
||||
suspend fun select(characterId: String, systemDark: Boolean) {
|
||||
if (!AppIconCatalog.isValid(characterId) || !available || !mutex.tryLock()) return
|
||||
try {
|
||||
mutableState.value = mutableState.value.copy(switching = true, error = null)
|
||||
controller.apply(characterId, systemDark)
|
||||
settings.appIconCharacterId = characterId
|
||||
mutableState.value = AppIconState(characterId)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
mutableState.value = mutableState.value.copy(error = error.message ?: error.toString())
|
||||
} finally {
|
||||
mutableState.value = mutableState.value.copy(switching = false)
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/** Native state wins after interrupted preference writes; repair missing/invalid native state. */
|
||||
suspend fun synchronize(systemDark: Boolean) {
|
||||
if (!available) return
|
||||
// Theme/resume reconciliation must not be dropped during a user switch.
|
||||
mutex.lock()
|
||||
try {
|
||||
mutableState.value = mutableState.value.copy(switching = true)
|
||||
val actual = controller.currentCharacterId()?.takeIf(AppIconCatalog::isValid)
|
||||
val requested = actual ?: settings.appIconCharacterId.takeIf(AppIconCatalog::isValid) ?: AppIconCatalog.DEFAULT
|
||||
controller.apply(requested, systemDark)
|
||||
settings.appIconCharacterId = requested
|
||||
mutableState.value = AppIconState(requested)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
mutableState.value = mutableState.value.copy(error = error.message ?: error.toString())
|
||||
} finally {
|
||||
mutableState.value = mutableState.value.copy(switching = false)
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() { mutableState.value = mutableState.value.copy(error = null) }
|
||||
}
|
||||
@@ -1,6 +1,19 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.i18n
|
||||
|
||||
internal val DeStrings: Map<String, String> = mapOf(
|
||||
"app_icon" to "App-Symbol",
|
||||
"app_icon_current" to "Aktuelles Symbol: %s",
|
||||
"app_icon_default" to "Standardsymbol",
|
||||
"app_icon_light" to "Helle Vorschau",
|
||||
"app_icon_dark" to "Dunkle Vorschau",
|
||||
"app_icon_restore" to "Standardsymbol wiederherstellen",
|
||||
"app_icon_selected" to "Ausgewählt",
|
||||
"app_icon_failed" to "Das Symbol konnte nicht geändert werden. Die bisherige Auswahl bleibt erhalten. Bitte erneut versuchen.",
|
||||
"app_icon_dismiss" to "Schließen",
|
||||
"app_icon_android_hint" to "Symbole folgen dem Systemdesign, wenn die App läuft oder erneut geöffnet wird. Der Launcher benötigt eventuell etwas Zeit.",
|
||||
"app_icon_ios_hint" to "Ab iOS 18 folgt das Symbol den Erscheinungsbild-Einstellungen des Home-Bildschirms. iOS 17 verwendet das helle Symbol.",
|
||||
"app_icon_unavailable" to "Dieses Gerät unterstützt keine alternativen App-Symbole.",
|
||||
|
||||
"open_drawer" to "Menü öffnen",
|
||||
"qr_camera_unavailable" to "Kamera nicht verfügbar. Bitte die PC-IP manuell eingeben.",
|
||||
"qr_scan_failed" to "Scan konnte nicht gestartet werden. Erneut versuchen oder die PC-IP manuell eingeben.",
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.i18n
|
||||
|
||||
internal val EnStrings: Map<String, String> = mapOf(
|
||||
"app_icon" to "App icon",
|
||||
"app_icon_current" to "Current icon: %s",
|
||||
"app_icon_default" to "Default icon",
|
||||
"app_icon_light" to "Light preview",
|
||||
"app_icon_dark" to "Dark preview",
|
||||
"app_icon_restore" to "Restore default icon",
|
||||
"app_icon_selected" to "Selected",
|
||||
"app_icon_failed" to "Could not change the icon. Your previous selection is retained. Try again.",
|
||||
"app_icon_dismiss" to "Dismiss",
|
||||
"app_icon_android_hint" to "Icons follow the system appearance while the app is running or when reopened. Launcher refresh may take a moment.",
|
||||
"app_icon_ios_hint" to "On iOS 18 and later, icons follow Home Screen appearance settings. iOS 17 uses the light icon.",
|
||||
"app_icon_unavailable" to "This device does not support alternate app icons.",
|
||||
|
||||
"open_drawer" to "Open menu",
|
||||
"qr_camera_unavailable" to "Camera unavailable. Please enter your PC IP manually.",
|
||||
"qr_scan_failed" to "Could not start scanning. Try again or enter your PC IP manually.",
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.i18n
|
||||
|
||||
internal val JaStrings: Map<String, String> = mapOf(
|
||||
"app_icon" to "アプリアイコン",
|
||||
"app_icon_current" to "現在のアイコン:%s",
|
||||
"app_icon_default" to "デフォルトアイコン",
|
||||
"app_icon_light" to "ライトのプレビュー",
|
||||
"app_icon_dark" to "ダークのプレビュー",
|
||||
"app_icon_restore" to "デフォルトに戻す",
|
||||
"app_icon_selected" to "選択中",
|
||||
"app_icon_failed" to "変更できませんでした。元のアイコンは保持されています。再試行してください。",
|
||||
"app_icon_dismiss" to "閉じる",
|
||||
"app_icon_android_hint" to "起動中または再起動時にシステムの外観に合わせます。ホーム画面への反映には時間がかかる場合があります。",
|
||||
"app_icon_ios_hint" to "iOS 18以降はホーム画面の外観設定に従います。iOS 17ではライトアイコンを使用します。",
|
||||
"app_icon_unavailable" to "このデバイスはアイコンの変更に対応していません。",
|
||||
|
||||
"open_drawer" to "メニューを開く",
|
||||
"qr_camera_unavailable" to "カメラを使用できません。PCのIPアドレスを手動で入力してください。",
|
||||
"qr_scan_failed" to "スキャンを開始できません。再試行するかPCのIPアドレスを手動で入力してください。",
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.i18n
|
||||
|
||||
internal val ZhStrings: Map<String, String> = mapOf(
|
||||
"app_icon" to "应用图标",
|
||||
"app_icon_current" to "当前图标:%s",
|
||||
"app_icon_default" to "默认图标",
|
||||
"app_icon_light" to "浅色预览",
|
||||
"app_icon_dark" to "暗色预览",
|
||||
"app_icon_restore" to "恢复默认图标",
|
||||
"app_icon_selected" to "已选择",
|
||||
"app_icon_failed" to "切换失败,原图标已保留,请重试",
|
||||
"app_icon_dismiss" to "关闭",
|
||||
"app_icon_android_hint" to "图标在应用运行或重新打开时跟随系统浅暗模式,桌面刷新可能稍有延迟。",
|
||||
"app_icon_ios_hint" to "iOS 18 起跟随桌面图标外观设置;iOS 17 使用浅色图标。",
|
||||
"app_icon_unavailable" to "此设备不支持切换应用图标",
|
||||
|
||||
"open_drawer" to "打开菜单",
|
||||
"qr_camera_unavailable" to "当前设备无法使用摄像头,请手动输入电脑 IP",
|
||||
"qr_scan_failed" to "无法启动扫码,请稍后重试或手动输入电脑 IP",
|
||||
|
||||
+5
-4
@@ -19,7 +19,7 @@ class LyricService(
|
||||
private val library: LibraryRepository,
|
||||
) {
|
||||
/** EULA 允许时为 0,否则为 1 */
|
||||
var lrcType: Int = if (PlatformInfo.allowEulaLyric) 0 else 1
|
||||
var lrcType: Int = if (PlatformInfo.extendedFeaturesEnabled) 0 else 1
|
||||
|
||||
/** 上次加载的完整歌词行。 */
|
||||
var fullLrc: Lyric = Lyric()
|
||||
@@ -58,7 +58,7 @@ class LyricService(
|
||||
val zh = handleLrc("zh", "ZH/$baseUrl$lyricPath", uid, forceRefresh)
|
||||
if (zh == null) ok = false else zhLrc = zh
|
||||
|
||||
if (PlatformInfo.allowEulaLyric) {
|
||||
if (PlatformInfo.extendedFeaturesEnabled) {
|
||||
val jp = handleLrc("jp", "JP/$baseUrl$lyricPath", uid, forceRefresh)
|
||||
if (jp == null) ok = false else jpLrc = jp
|
||||
val roma = handleLrc("roma", "ROMA/$baseUrl$lyricPath", uid, forceRefresh)
|
||||
@@ -141,7 +141,7 @@ class LyricService(
|
||||
translation = null
|
||||
}
|
||||
1 -> {
|
||||
if (PlatformInfo.allowEulaLyric) {
|
||||
if (PlatformInfo.extendedFeaturesEnabled) {
|
||||
main = lyric.jp
|
||||
translation = lyric.zh
|
||||
} else {
|
||||
@@ -183,7 +183,8 @@ data class LyricFetchResult(
|
||||
)
|
||||
|
||||
expect object PlatformInfo {
|
||||
val allowEulaLyric: Boolean
|
||||
/** Shared unlock for extended lyrics and custom app icons; fixed for this process. */
|
||||
val extendedFeaturesEnabled: Boolean
|
||||
/** 是否为 iOS 平台,本地音频使用 wav 而非 flac。 */
|
||||
val isIos: Boolean
|
||||
/** 对外展示版本号(iOS `CFBundleShortVersionString`)。 */
|
||||
|
||||
+3
@@ -195,6 +195,7 @@ object DeepLinkParser {
|
||||
* 调用 `PlatformServices.install` 前默认为 NoOp。
|
||||
*/
|
||||
object PlatformServices {
|
||||
var appIcon: com.zhushenwudi.lovelivemusicplayer.appicon.AppIconController = com.zhushenwudi.lovelivemusicplayer.appicon.UnsupportedAppIconController
|
||||
var desktopLyric: DesktopLyricController = NoOpDesktopLyric
|
||||
var homeWidget: HomeWidgetController = NoOpHomeWidget
|
||||
var deepLink: DeepLinkHandler = NoOpDeepLink
|
||||
@@ -207,6 +208,7 @@ object PlatformServices {
|
||||
var carPlayCatalog: CarPlayCatalog = CarPlayCatalog()
|
||||
|
||||
fun install(
|
||||
appIcon: com.zhushenwudi.lovelivemusicplayer.appicon.AppIconController = this.appIcon,
|
||||
desktopLyric: DesktopLyricController = this.desktopLyric,
|
||||
homeWidget: HomeWidgetController = this.homeWidget,
|
||||
deepLink: DeepLinkHandler = this.deepLink,
|
||||
@@ -218,6 +220,7 @@ object PlatformServices {
|
||||
pip: PictureInPictureHook = this.pip,
|
||||
carPlayCatalog: CarPlayCatalog = this.carPlayCatalog,
|
||||
) {
|
||||
this.appIcon = appIcon
|
||||
this.desktopLyric = desktopLyric
|
||||
this.homeWidget = homeWidget
|
||||
this.deepLink = deepLink
|
||||
|
||||
+4
@@ -5,6 +5,10 @@ import com.russhwolf.settings.set
|
||||
import com.zhushenwudi.lovelivemusicplayer.player.LoopMode
|
||||
|
||||
class AppSettings(private val settings: Settings) {
|
||||
var appIconCharacterId: String
|
||||
get() = settings.getString(SettingsKeys.APP_ICON_CHARACTER_ID, "default")
|
||||
set(value) = settings.set(SettingsKeys.APP_ICON_CHARACTER_ID, value)
|
||||
|
||||
var isDark: Boolean
|
||||
get() = settings.getBoolean(SettingsKeys.DARK, false)
|
||||
set(value) = settings.set(SettingsKeys.DARK, value)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.settings
|
||||
|
||||
object SettingsKeys {
|
||||
const val APP_ICON_CHARACTER_ID = "SP_APP_ICON_CHARACTER_ID"
|
||||
const val DARK = "SP_IS_DARK"
|
||||
const val WITH_SYSTEM_THEME = "SP_With_System_Theme"
|
||||
const val LOOP_MODE = "SP_LOOP_MODE"
|
||||
|
||||
+3
-2
@@ -20,14 +20,15 @@ private const val UNLOCK_DIR_NAME = "LLMP"
|
||||
|
||||
actual object PlatformInfo {
|
||||
/**
|
||||
* iOS 默认只提供中文歌词;Documents 下存在 LLMP 目录才解锁日文/罗马音。
|
||||
* iOS 默认只提供中文歌词;Documents 下存在 LLMP 目录才解锁扩展功能
|
||||
* (日文/罗马音歌词、自定义应用图标)。
|
||||
*
|
||||
* 刻意只在首次读取时查一次盘:[LyricService.lrcType] 的初值、歌词下载分支、
|
||||
* 以及多处 UI 分支都依赖这个值,运行中途翻转会让它们对不上
|
||||
* (例如 lrcType 停在 0 要显示日文,而日文根本没下载过)。
|
||||
* 用户新建目录后重启应用生效。
|
||||
*/
|
||||
actual val allowEulaLyric: Boolean by lazy { unlockDirExists() }
|
||||
actual val extendedFeaturesEnabled: Boolean by lazy { unlockDirExists() }
|
||||
actual val isIos: Boolean = true
|
||||
actual val appVersion: String =
|
||||
NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.platform
|
||||
|
||||
import com.zhushenwudi.lovelivemusicplayer.appicon.AppIconCatalog
|
||||
import com.zhushenwudi.lovelivemusicplayer.appicon.AppIconController
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import platform.UIKit.*
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSNotificationCenter
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
class IosAppIconController : AppIconController {
|
||||
override val supported: Boolean get() = UIApplication.sharedApplication.supportsAlternateIcons
|
||||
override val appearanceHintKey = "app_icon_ios_hint"
|
||||
override suspend fun publishArtwork(characterId: String, light: ByteArray?, dark: ByteArray?) = withContext(Dispatchers.Main) {
|
||||
require(AppIconCatalog.isValid(characterId))
|
||||
val fingerprint = "$characterId:${light?.contentHashCode()}:${dark?.contentHashCode()}"
|
||||
val container = NSFileManager.defaultManager.containerURLForSecurityApplicationGroupIdentifier(IOS_APP_GROUP)
|
||||
?: error("App Group container unavailable")
|
||||
fun exists(mode: String): Boolean = container.URLByAppendingPathComponent("app-icon-$characterId-$mode.png")?.path
|
||||
?.let { NSFileManager.defaultManager.fileExistsAtPath(it) } == true
|
||||
if (WidgetDataWriter.readString("appIconArtworkFingerprint") == fingerprint &&
|
||||
(characterId == AppIconCatalog.DEFAULT || (exists("light") && exists("dark")))) return@withContext
|
||||
if (characterId != AppIconCatalog.DEFAULT) {
|
||||
require(light != null && dark != null)
|
||||
WidgetDataWriter.writeFile("app-icon-$characterId-light.png", light)
|
||||
WidgetDataWriter.writeFile("app-icon-$characterId-dark.png", dark)
|
||||
check(exists("light") && exists("dark")) { "App icon preview publication failed" }
|
||||
}
|
||||
WidgetDataWriter.writeString("appIconCharacterId", characterId)
|
||||
WidgetDataWriter.writeString("appIconArtworkFingerprint", fingerprint)
|
||||
NSNotificationCenter.defaultCenter.postNotificationName("llmpAppIconArtworkChanged", null)
|
||||
NSNotificationCenter.defaultCenter.postNotificationName("llmpReloadWidgets", null)
|
||||
}
|
||||
override suspend fun currentCharacterId(): String? = withContext(Dispatchers.Main) {
|
||||
val name = UIApplication.sharedApplication.alternateIconName
|
||||
if (name == null) AppIconCatalog.DEFAULT else
|
||||
AppIconCatalog.characters.firstOrNull { AppIconCatalog.iosName(it.id) == name }?.id
|
||||
}
|
||||
override suspend fun apply(characterId: String, systemDark: Boolean): Unit = withContext(Dispatchers.Main) {
|
||||
require(AppIconCatalog.isValid(characterId))
|
||||
val app = UIApplication.sharedApplication
|
||||
check(app.supportsAlternateIcons) { "Alternate app icons are unavailable" }
|
||||
val name = AppIconCatalog.iosName(characterId)
|
||||
if (app.alternateIconName == name) return@withContext
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
app.setAlternateIconName(name) { error ->
|
||||
if (continuation.isActive) {
|
||||
if (error == null) continuation.resume(Unit)
|
||||
else continuation.resumeWithException(IllegalStateException(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -356,6 +356,7 @@ class IosMediaSessionBridge : MediaSessionHook {
|
||||
|
||||
fun installIosPlatformServices() {
|
||||
PlatformServices.install(
|
||||
appIcon = IosAppIconController(),
|
||||
desktopLyric = IosDesktopLyricController(),
|
||||
homeWidget = IosHomeWidgetController(),
|
||||
deepLink = IosDeepLinkHandler(),
|
||||
|
||||
Reference in New Issue
Block a user