add config icon feature

This commit is contained in:
2026-10-09 22:41:45 +08:00
parent 493c70ce81
commit aa028266ee
1621 changed files with 5130 additions and 77 deletions
@@ -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 = ""
}
@@ -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('-', '_')
}
@@ -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",
@@ -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`)。 */
@@ -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
@@ -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,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"
@@ -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
@@ -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))
}
}
}
}
}
@@ -356,6 +356,7 @@ class IosMediaSessionBridge : MediaSessionHook {
fun installIosPlatformServices() {
PlatformServices.install(
appIcon = IosAppIconController(),
desktopLyric = IosDesktopLyricController(),
homeWidget = IosHomeWidgetController(),
deepLink = IosDeepLinkHandler(),