diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2d9b6f9..1e9dc8b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -61,6 +61,13 @@ jobs: set -o pipefail ./gradlew lintDebug --console=plain 2>&1 | tee ../gradle-lint.log + - name: Kotlin 格式检查(ktlint) + run: | + set -o pipefail + curl -sSLo /tmp/ktlint https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint + chmod +x /tmp/ktlint + /tmp/ktlint --relative "app/src/**/*.kt" 2>&1 | tee ../gradle-ktlint.log + - name: 构建 Debug APK run: | set -o pipefail diff --git a/.gitignore b/.gitignore index ace0093..36bd150 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ pnpm-debug.log* *.pem *.key *private-key* +*.p12 +*.jks +*.keystore +/android/keystore diff --git a/android/.editorconfig b/android/.editorconfig new file mode 100644 index 0000000..dcd07cf --- /dev/null +++ b/android/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{kt,kts}] +# 与 Kotlin 官方风格一致;行宽交给 IDE 排版,不在 CI 卡 140 列 +ktlint_code_style = intellij_idea +max_line_length = off +# Compose 组件按约定使用 PascalCase;单表达式函数强制写回一行也与现有风格冲突 +ktlint_standard_function-naming = disabled +ktlint_standard_function-signature = disabled diff --git a/android/README.md b/android/README.md index 2ff5cc4..08defe6 100644 --- a/android/README.md +++ b/android/README.md @@ -78,20 +78,60 @@ PR 与 push 到 `main` / `master` 时触发,仅在 `android/**` 有变更时 ``` app/src/main/java/dev/amll/hub/android/ -├── MainActivity.kt # 入口,处理 amllhub:// deep link +├── MainActivity.kt # 入口,处理 amllhub:// deep link ├── data/ -│ ├── local/AuthStore.kt # DataStore 存 token -│ └── remote/ # Retrofit API、DTO、错误映射 -├── di/AppModule.kt # Hilt 模块 -├── nav/AmllNavGraph.kt # 登录 / 首页 / 扫码 +│ ├── local/AuthStore.kt # DataStore 存 token +│ ├── lyric/TtmlParser.kt # TTML → 逐字行(翻译 / 音译 / 背景行) +│ ├── remote/ # Retrofit API、DTO、错误映射 +│ └── repo/ContentRepository.kt# 内容仓库,接口失败降级为空 +├── playback/ +│ ├── PlayerController.kt # Media3 ExoPlayer + 播放状态 StateFlow +│ └── PlaybackService.kt # MediaSessionService,后台播放与通知栏 +├── di/AppModule.kt # Hilt 模块 +├── nav/AmllNavGraph.kt # 登录态 → 5 Tab + 二级页 + 启动页 └── ui/ - ├── component/ # QrScannerView、ErrorBanner - ├── screen/ # Login / Home / Scan + ViewModel - └── theme/ # 颜色与字体,对齐网页端 token + ├── AppNav.kt # 页面跳转集合 + ├── AppViewModel.kt # Activity 级共享状态(首页/榜单/歌词/播放) + ├── component/Common.kt # TopBar / PillTabBar / SongRow / EmptyState… + ├── screen/ + │ ├── MainScreens.kt # 首页 / 发现 / 排行榜 / 每日推荐 / 消息 / 我的 + │ └── FlowScreens.kt # 歌词详情 / 沉浸播放 / 创作 / 投稿 / 稿件 / 审核 + └── theme/ # Palette(6 组语义色)+ Dimens(1:1 token) ``` +## 沉浸播放的歌词页(react-full) + +全屏沉浸播放用 WebView 承载网页端同款歌词渲染(`@applemusic-like-lyrics/react-full` 的 +`PrebuiltLyricPlayer` + jotai atoms),源码在 `android/lyrics-web/`,构建产物直接落到 +`app/src/main/assets/lyrics/`,APK 内离线可用。 + +```bash +cd android/lyrics-web + +# 复用 frontend 已装好的依赖(避免重复下载) +# Windows: New-Item -ItemType Junction -Path node_modules -Target ../../frontend/node_modules +# macOS/Linux: ln -s ../../frontend/node_modules node_modules + +node ../../frontend/node_modules/vite/bin/vite.js build +``` + +原生侧通过 `WebViewAssetLoader` 以 `https://appassets.androidplatform.net/assets/lyrics/index.html` +加载,避免 `file://` 下 ES Module 的跨域限制;进度与播放态由 `PlayerController` 通过 +`window.amllNative` 写入。 + ## 待办 -- 个人中心(资料编辑、头像上传、密码修改) -- 投稿与歌词浏览 +- 游客态浏览:当前沿用登录门禁,未登录只显示登录页 +- 投稿提交:表单已还原,但 TTML 解析与文件上传未接,提交按钮暂未落库 +- 头像上传、审核操作(通过 / 驳回)、每日推荐「下载图片」 +- 稿件详情的「原文件 / 文件效果」对比视图 - 正式签名配置(当前 release 走 debug key) + +## 已接通的交互 + +- 首页 / 发现的搜索框 → 半屏搜索弹层(词库 `/api/v1/search` + 网易云 `/api/v1/ncm/search`) +- 榜单 / 最新收录 / 推荐卡 → 歌词详情;封面点按 → WebView 沉浸播放 +- 每日推荐点赞(乐观更新 + 失败回滚)、消息全部已读 / 单条已读 +- 我的页读 `/api/v1/auth/profile`;编辑资料保存昵称走 `PUT /api/v1/auth/profile` +- 创作中心稿件列表 → 稿件详情(`GET /api/v1/submissions/{id}`) + diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index cb4ddc4..0c5d084 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,24 +9,36 @@ plugins { alias(libs.plugins.ksp) } -// 从 local.properties 或环境变量读 API Base URL,缺失时回落到 beta 线上后端 -val apiBaseUrl: String = run { - val fromLocal = rootProject.file("local.properties") - .takeIf { it.exists() } - ?.let { file -> Properties().apply { file.inputStream().use(::load) }.getProperty("amll.api.baseUrl") } - val fromEnv = System.getenv("AMLL_API_BASE_URL") - fromLocal?.takeIf { it.isNotBlank() } - ?: fromEnv?.takeIf { it.isNotBlank() } - ?: "https://api.beta.amllhub.bikonoo.com" -} +// local.properties 不进版本库(CI 用环境变量),两项都缺时才回落到 beta 后端 +val localProperties: Properties = rootProject.file("local.properties") + .takeIf { it.exists() } + ?.let { file -> Properties().apply { file.inputStream().use(::load) } } + ?: Properties() + +fun localOrEnv(key: String, env: String): String? = + localProperties.getProperty(key)?.takeIf { it.isNotBlank() } + ?: System.getenv(env)?.takeIf { it.isNotBlank() } + +val apiBaseUrl: String = localOrEnv("amll.api.baseUrl", "AMLL_API_BASE_URL") + ?: "https://api.beta.amllhub.bikonoo.com" + +// 发布签名:四项齐全才启用正式 keystore,否则 release 继续用 debug key 兜底 +val releaseStoreFile = localOrEnv("amll.release.storeFile", "AMLL_RELEASE_STORE_FILE") +val releaseStorePassword = localOrEnv("amll.release.storePassword", "AMLL_RELEASE_STORE_PASSWORD") +val releaseKeyAlias = localOrEnv("amll.release.keyAlias", "AMLL_RELEASE_KEY_ALIAS") +val releaseKeyPassword = localOrEnv("amll.release.keyPassword", "AMLL_RELEASE_KEY_PASSWORD") +val hasReleaseSigning = listOf(releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword) + .all { !it.isNullOrBlank() } android { + // namespace 是代码包名(R / BuildConfig 的包路径),与安装包标识无关, + // 这里保持不动;对外显示的包名是下面的 applicationId namespace = "dev.amll.hub.android" // 依赖(Coil 3.6 / Compose 1.12 / Navigation 2.10 等)要求 compileSdk 37 compileSdk = 37 defaultConfig { - applicationId = "dev.amll.hub.android" + applicationId = "com.amlldev.amllhub" minSdk = 26 // targetSdk 与 compileSdk 独立:这里不跟随升级, // 避免在验证编译阶段同时引入 Android 17 的运行时行为变更 @@ -38,18 +50,35 @@ android { buildConfigField("String", "API_BASE_URL", "\"$apiBaseUrl\"") } + signingConfigs { + if (hasReleaseSigning) { + create("release") { + storeFile = rootProject.file(releaseStoreFile!!) + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + storeType = "PKCS12" + } + } + } + buildTypes { debug { isMinifyEnabled = false - applicationIdSuffix = ".debug" + // 不加 applicationIdSuffix:debug 包也用 com.amlldev.amllhub, + // 免得装出来的包名和 release 不一致 versionNameSuffix = "-debug" } release { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - // 未配置签名时用debug key,保证 assembleRelease 也能跑通(CI 只出 debug 包) - signingConfig = signingConfigs.getByName("debug") + // 配置齐全时用正式 keystore;否则用 debug key 兜底,保证 assembleRelease 也能跑通 + signingConfig = if (hasReleaseSigning) { + signingConfigs.getByName("release") + } else { + signingConfigs.getByName("debug") + } } } @@ -85,6 +114,9 @@ kotlin { dependencies { implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + // XML 主题(Theme.Material3.DayNight.NoActionBar) + implementation(libs.material) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) @@ -94,6 +126,7 @@ dependencies { // Compose(版本由 BOM 统一管理) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) + implementation(libs.androidx.animation) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) @@ -102,6 +135,16 @@ dependencies { implementation(libs.androidx.navigation.compose) + // 播放(Media3)与内嵌歌词页 + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.webkit) + + // 下载标签:MP3(mp3agic)/ FLAC(jaudiotagger) + implementation(libs.mp3agic) + implementation(libs.jaudiotagger) + implementation(libs.slf4j.nop) + // DI implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) @@ -121,6 +164,10 @@ dependencies { implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) + // 底栏 / 播放条的实时背景模糊 + implementation(libs.haze.core) + implementation(libs.haze.blur) + // 扫码 implementation(libs.mlkit.barcode.scanning) implementation(libs.camera.camera2) diff --git a/android/app/src/debug/res/xml/network_security_config.xml b/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 0000000..ad043b9 --- /dev/null +++ b/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + 10.0.2.2 + localhost + 127.0.0.1 + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9fc101d..bc142bd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,13 +5,26 @@ + + + + + + + + + + android:theme="@style/Theme.AmllHub.Starting"> @@ -37,5 +50,21 @@ + + + + + + + + + + diff --git a/android/app/src/main/assets/captcha/index.html b/android/app/src/main/assets/captcha/index.html new file mode 100644 index 0000000..d241d35 --- /dev/null +++ b/android/app/src/main/assets/captcha/index.html @@ -0,0 +1,131 @@ + + + + + + + + +
+
+
等待初始化…
+
+ + + diff --git a/android/app/src/main/assets/lyrics/assets/index-D6zGEe-5.css b/android/app/src/main/assets/lyrics/assets/index-D6zGEe-5.css new file mode 100644 index 0000000..91d5cbc --- /dev/null +++ b/android/app/src/main/assets/lyrics/assets/index-D6zGEe-5.css @@ -0,0 +1 @@ +.amll-lyric-player{width:100%;max-width:100%;height:100%;color:var(--amll-lp-color, white);contain:strict;mix-blend-mode:plus-lighter;font-size:var(--amll-lp-font-size, max(max(5vh, 2.5vw), 12px));overflow:hidden}@media screen and (max-width:768px){.amll-lyric-player{font-size:var(--amll-lp-font-size, max(8vw, 12px))}}.amll-lyric-player.dom{--amll-lp-line-width-aspect: .8;--amll-lp-line-padding-x: 1em;--amll-lp-bg-line-scale: .7;-webkit-user-select:none;user-select:none;box-sizing:content-box;z-index:1;line-height:1.2}@media screen and (max-width:768px){.amll-lyric-player{--amll-lp-line-width-aspect: 1;--amll-lp-line-padding-x: 0}}.FmKaba_lyricLineWrapper{box-sizing:border-box;width:100%;padding:.4em var(--lyric-line-padding-x);will-change:transform,opacity,filter;border-radius:.25em;flex-direction:column;align-items:flex-start;gap:.3em;transition:opacity .4s,filter .4s,background-color .25s;display:flex;position:absolute;top:0;left:0}.FmKaba_lyricLineWrapper:hover{background-color:var(--amll-lp-hover-bg-color, #fff1)}.FmKaba_lyricLineWrapper:active{background-color:var(--amll-lp-hover-bg-color, #ffffff05)}.FmKaba_lyricLine{box-sizing:border-box;width:var(--amll-lp-width, 100%);min-width:var(--amll-lp-width, 100%);max-width:var(--amll-lp-width, 100%);contain:layout style paint;content-visibility:auto;contain-intrinsic-size:auto 40px;backface-visibility:hidden;transform-origin:0;will-change:transform,filter,opacity;height:fit-content;margin:-.2em;padding:.2em;transition:box-shadow .25s;position:relative}.FmKaba_lyricDuetLine{text-align:right;transform-origin:100%}.FmKaba_lyricMainLine{contain:layout style;margin:-1em;padding:1em;transition:opacity .3s .1s}.FmKaba_lyricMainLine span{text-align:start;vertical-align:bottom;display:inline-block}.FmKaba_lyricMainLine .FmKaba_romanWord{padding-inline-end:.3em;font-size:.5em;line-height:1em;display:flex}.FmKaba_lyricMainLine .FmKaba_rubyWord{justify-content:center;min-height:1em;font-size:.5em;line-height:1em;display:flex}.FmKaba_lyricMainLine .FmKaba_wordWithRuby{vertical-align:bottom;flex-direction:column;align-items:center;display:inline-flex}.FmKaba_lyricMainLine .FmKaba_wordBody{flex-direction:column;align-items:center;display:flex}.FmKaba_lyricMainLine>span,.FmKaba_lyricMainLine span.FmKaba_emphasizeWrapper{white-space:pre-wrap;vertical-align:bottom;will-change:transform,text-shadow;contain:layout style;margin:-1em;padding:1em;display:inline-block}:is(.FmKaba_lyricMainLine>span,.FmKaba_lyricMainLine span.FmKaba_emphasizeWrapper).FmKaba_emphasize,:is(.FmKaba_lyricMainLine>span,.FmKaba_lyricMainLine span.FmKaba_emphasizeWrapper) span.FmKaba_emphasize{backface-visibility:hidden;margin:-1em;padding:1em}:is(:is(.FmKaba_lyricMainLine>span,.FmKaba_lyricMainLine span.FmKaba_emphasizeWrapper).FmKaba_emphasize,:is(.FmKaba_lyricMainLine>span,.FmKaba_lyricMainLine span.FmKaba_emphasizeWrapper) span.FmKaba_emphasize)>span{will-change:transform,text-shadow;backface-visibility:hidden;margin:-1em;padding:1em}.FmKaba_lyricBgLine{opacity:.4;font-size:max(calc(1em * var(--amll-lp-bg-line-scale, .7)),10px);transition:background-color .25s,box-shadow .25s}.FmKaba_lyricBgLine .FmKaba_lyricMainLine{padding:1.2em 1em}.FmKaba_lyricBgLine.FmKaba_active{opacity:.4;transition:background-color .25s,box-shadow .25s}.FmKaba_lyricSubLine{opacity:.3;font-size:max(.5em,10px);line-height:1.5em;transition:opacity .2s .25s}@supports (mix-blend-mode: plus-lighter){.FmKaba_lyricSubLine{opacity:.3}}.FmKaba_bottomLine{cursor:default;padding-top:0;padding-bottom:0;line-height:1.8em}.FmKaba_bottomLine:empty{height:0;margin:0;padding:0;display:none}.FmKaba_bgWrapper{top:100%;left:var(--lyric-line-padding-x);z-index:-1;align-items:inherit;width:calc(100% - var(--lyric-line-padding-x) * 2);visibility:visible;pointer-events:auto;opacity:0;transform-origin:0 0;flex-direction:column;transition:opacity .3s;display:flex;position:absolute}.FmKaba_bgWrapperTop{transform-origin:0 100%;width:100%;margin-top:-999px;position:relative;top:auto;bottom:auto;left:0}.FmKaba_bgWrapperActive{opacity:1;width:100%;position:relative;top:auto;bottom:auto;left:0}.FmKaba_bgWrapperHidden{visibility:hidden;pointer-events:none}.FmKaba_interludeDots{opacity:0;transform-origin:center;gap:.25em;width:fit-content;height:clamp(.5em,1vh,3em);padding:2.5% .75em;transition:opacity .25s;display:flex;position:absolute;left:0}.FmKaba_interludeDots.FmKaba_enabled{opacity:1}.FmKaba_interludeDots>*{background-color:var(--amll-lp-color, white);aspect-ratio:1;border-radius:50%;width:clamp(.5em,1vh,3em);height:clamp(.5em,1vh,3em);margin-right:4px;display:inline-block}.FmKaba_interludeDots.FmKaba_duet{transform-origin:center;right:0}.FmKaba_disableSpring>*,.FmKaba_disableSpring .FmKaba_lyricLine{transition:filter .25s,transform .5s,background-color .25s,box-shadow .25s}.FmKaba_tmpDisableTransition{transition:none!important}.amll-lyric-player{--lyric-line-padding-x: 1em}@media screen and (max-width:500px){.amll-lyric-player{--lyric-line-padding-x: 20px}}.amll-lyric-player:hover .FmKaba_lyricLine,.amll-lyric-player:hover .FmKaba_lyricLineWrapper{filter:unset!important}.amll-lyric-player:not(.FmKaba_playing) .FmKaba_bgWrapper{opacity:1;width:100%;position:relative;top:auto;bottom:auto;left:0}.amll-lyric-player.FmKaba_hasDuetLine .FmKaba_lyricLine:not(.FmKaba_lyricDuetLine){padding-right:15%}.amll-lyric-player.FmKaba_hasDuetLine .FmKaba_lyricDuetLine{padding-left:15%}.amll-lyric-player.FmKaba_hasDuetLine .FmKaba_lyricLineWrapper:has(.FmKaba_lyricDuetLine){align-items:flex-end}.amll-lyric-player.FmKaba_hasDuetLine .FmKaba_lyricLineWrapper:has(.FmKaba_lyricDuetLine) .FmKaba_bgWrapper{transform-origin:100% 0}.amll-lyric-player.FmKaba_hasDuetLine .FmKaba_lyricLineWrapper:has(.FmKaba_lyricDuetLine) .FmKaba_bgWrapperTop{transform-origin:100% 100%}.nNqm_a_audioQualityTag{overflow:hidden}@keyframes nNqm_a_dolby-glow{0%{opacity:1}to{opacity:0}}.nNqm_a_dolbyLogoGlow{filter:blur(7px);opacity:1;margin-top:2.5px;animation-name:nNqm_a_dolby-glow;animation-duration:3s;animation-timing-function:ease-in-out;animation-delay:1s;animation-fill-mode:forwards;position:absolute}.nNqm_a_clickable{cursor:pointer}.nNqm_a_commonTag{color:#fff;box-sizing:border-box;align-items:center;padding:2px .52em 2px .1em;display:flex;position:relative}.nNqm_a_commonTag:before{content:"";opacity:.2;border-image-source:url(data:image/svg+xml,%3Csvg%20width%3D%2236%22%20height%3D%2236%22%20viewBox%3D%220%200%2036%2036%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpath%0A%20%20%20%20%20%20%20%20d%3D%22M19.5%200C25.573%200%2028.6095%200%2030.8862%201.27504C32.4953%202.17617%2033.8238%203.50471%2034.725%205.11379C36%207.39054%2036%2010.427%2036%2016.5V19.5C36%2025.573%2036%2028.6095%2034.725%2030.8862C33.8238%2032.4953%2032.4953%2033.8238%2030.8862%2034.725C28.6095%2036%2025.573%2036%2019.5%2036H16.5C10.427%2036%207.39054%2036%205.11379%2034.725C3.50471%2033.8238%202.17617%2032.4953%201.27504%2030.8862C0%2028.6095%200%2025.573%200%2019.5L0%2016.5C0%2010.427%200%207.39054%201.27504%205.11379C2.17617%203.50471%203.50471%202.17617%205.11379%201.27504C7.39054%200%2010.427%200%2016.5%200L19.5%200Z%22%0A%20%20%20%20%20%20%20%20fill%3D%22white%22%20%2F%3E%0A%3C%2Fsvg%3E);border-image-slice:15 fill;border-image-width:8.5px;width:100%;height:100%;position:absolute;top:0;left:0}.nNqm_a_commonTagText{letter-spacing:.01809em;white-space:nowrap;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Pingfang SC;font-size:11px;font-weight:500}.ah9gFq_nowPlayingSlider{touch-action:none;cursor:pointer;justify-content:stretch;align-items:center;min-height:24px;display:flex;transform:translateZ(0)}.ah9gFq_nowPlayingSlider>.ah9gFq_inner{background-color:#ffffff26;flex:1;width:100%;height:20px}.ah9gFq_nowPlayingSlider>.ah9gFq_inner>.ah9gFq_thumb{opacity:.4;transform-origin:0;background-color:#fff;width:100%;height:100%}.ah9gFq_nowPlayingSlider .ah9gFq_nowPlayingSlider>svg{opacity:.5}.OWkF6W_controlThumb{mix-blend-mode:plus-lighter;justify-self:center;width:100%;height:100%}.OWkF6W_controlThumb button{touch-action:none;mix-blend-mode:plus-lighter;cursor:none;background-color:#ffffff2d;border:none;border-radius:4px;padding:0;display:block;position:absolute}.OWkF6W_controlThumb button div{background-color:#000;border-radius:10px;position:absolute}.Q2PpKa_cover{--base-box-shadow-v-0: #00000030;--base-box-shadow-y-0: 1em;--base-box-shadow-r-0: 1.2em;aspect-ratio:1;object-fit:cover;object-position:center;width:100%;height:100%;filter:drop-shadow(var(--base-box-shadow-v-0) 0px var(--base-box-shadow-y-0) var(--base-box-shadow-r-0));background-position:center;background-size:cover;transition:background-image .5s linear,filter .5s,transform .5s cubic-bezier(.3,.2,.2,1.4);transform:scale(1)}.Q2PpKa_cover.Q2PpKa_musicPaused{--base-box-shadow-v-0: #00000030;--base-box-shadow-y-0: .8em;--base-box-shadow-r-0: .8em;transform:scale(var(--scale-level));transition:background-image .5s linear,filter .5s,transform .6s cubic-bezier(.4,.2,.1,1)}.Q2PpKa_coverInner{background-color:#000;background-position:center;background-size:cover;width:100%;height:100%;transition:background-image .5s linear}@keyframes _7JvWRa_pressed-animation{0%{transform:scale(1)}20%{transform:scale(.85)}50%{transform:scale(1.1)}to{transform:scale(1)}}._7JvWRa_mediaButton{aspect-ratio:1;background-color:#fff0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:background-color .3s;display:flex;position:relative}._7JvWRa_mediaButton>*{transform-origin:center;transition:transform .5s}._7JvWRa_mediaButton:hover{background-color:#fff2}._7JvWRa_mediaButton._7JvWRa_animate>*{animation-composition:accumulate;animation:.7s _7JvWRa_pressed-animation}._7JvWRa_mediaButton:active{background-color:#fff2}._7JvWRa_mediaButton:active *{animation-name:none}.GOs7HW_menuButton{aspect-ratio:1;background:#ffffff15;border:none;border-radius:50%;justify-content:center;align-self:center;align-items:center;width:3.5vh;margin-left:16px;padding:0;display:flex;position:relative}.GOs7HW_menuButton svg{color:#fff;width:72%;height:auto}@media screen and (max-width:1000px){.GOs7HW_menuButton{width:max(2em,min(5vw,4vh))}}.Dbqjoq_textMarquee{min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(to right,#000 95%,#0000)}.Dbqjoq_textMarquee.Dbqjoq_animating{-webkit-mask-image:linear-gradient(to left,#0000,#000 5% 95%,#0000)}.Dbqjoq_textMarquee>*{width:fit-content}.XWzuDW_musicInfo{color:#fff;min-width:0;font-size:max(2vh,1em);display:flex}.XWzuDW_info{flex:1;min-width:0;line-height:1.25em;overflow:hidden}.XWzuDW_name{white-space:nowrap;cursor:text;-webkit-user-select:text;user-select:text;letter-spacing:.4px;opacity:.9;mix-blend-mode:normal;min-width:0;font-weight:500}.XWzuDW_artists,.XWzuDW_album{white-space:nowrap;text-align:center;opacity:.45;letter-spacing:.4px;font-weight:400}:is(.XWzuDW_artists,.XWzuDW_album) a{-webkit-user-select:text;user-select:text;text-decoration:none}:is(.XWzuDW_artists,.XWzuDW_album) a:hover{opacity:.75}:is(.XWzuDW_artists,.XWzuDW_album) a:active{opacity:.5}:is(.XWzuDW_artists,.XWzuDW_album) a:after{content:var(--applemusic-like-lyrics-music-artists-separator, ", ")}:is(.XWzuDW_artists,.XWzuDW_album) a:nth-last-child(2):after{content:" & "}:is(.XWzuDW_artists,.XWzuDW_album) a:last-child:after{content:""}._8f42YG_background{width:100%;height:100%;transition:all .5s ease-in-out;position:absolute;left:0%}._8f42YG_background:after{content:"";background-image:linear-gradient(#0000 60%,#0000001a);width:100vw;height:100vh;position:absolute;top:0;left:0}.-DaA0W_thumb{will-change:transform;mix-blend-mode:plus-lighter;grid-area:-DaA0W_thumb / -DaA0W_info-side;justify-self:center;margin:2vh;position:relative}.-DaA0W_cover{aspect-ratio:1;width:var(--horizontal-layout-max-width);height:var(--horizontal-layout-max-width);grid-area:-DaA0W_cover / -DaA0W_info-side;place-self:center;position:relative}.-DaA0W_controls{will-change:transform;mix-blend-mode:plus-lighter;min-width:0;min-height:0;width:var(--horizontal-layout-max-width);flex-direction:column;grid-area:-DaA0W_music-info / -DaA0W_info-side;justify-content:space-between;justify-self:center;margin-top:calc(1.75em - 8px);display:flex;position:relative}.-DaA0W_lyric{box-sizing:border-box;mix-blend-mode:plus-lighter;contain:paint;grid-area:2 / -DaA0W_player-side / 5;width:100%;height:100%;padding-right:15%;transition:opacity .5s cubic-bezier(.5,0,.5,1) .25s;-webkit-mask-image:linear-gradient(#0000,#000 10% 90%,#0000);mask-image:linear-gradient(#0000,#000 10% 90%,#0000)}@media screen and (max-width:1600px),(max-height:1000px){.-DaA0W_lyric{padding-right:8%}}.-DaA0W_bottomControls{mix-blend-mode:plus-lighter;flex-direction:row-reverse;grid-area:-DaA0W_buttom-controls / 1 / -DaA0W_buttom-controls / 4;gap:4em;padding-left:4em;padding-right:4em;display:flex}@media screen and (max-width:1600px),(max-height:1000px){.-DaA0W_bottomControls{gap:2em;padding-left:2em;padding-right:2em}}.-DaA0W_horizontalLayout{--hide-lyric-left: 61.1111%;grid-template-rows:[-DaA0W_drag-area] minmax(30px,.45fr)[-DaA0W_thumb] auto[-DaA0W_cover] auto[-DaA0W_music-info] 3fr[-DaA0W_buttom-controls] 0fr .3fr;grid-template-columns:[-DaA0W_info-side] .45fr[-DaA0W_player-side] .55fr[-DaA0W_side-controls] 0fr;gap:8px;width:100%;height:100%;transition:all .5s ease-in-out;display:grid;position:relative;left:0%;--horizontal-layout-max-width: min(50vh, 38vw)}.-DaA0W_horizontalLayout .-DaA0W_thumb,.-DaA0W_horizontalLayout .-DaA0W_cover,.-DaA0W_horizontalLayout .-DaA0W_controls{transition:left .5s cubic-bezier(.5,0,.5,1);left:0%}.-DaA0W_horizontalLayout.-DaA0W_hideLyric .-DaA0W_lyric{opacity:0;pointer-events:none;transition:opacity .25s cubic-bezier(.5,0,.5,1),transform .5s cubic-bezier(.5,0,.5,1)}.-DaA0W_horizontalLayout.-DaA0W_hideLyric .-DaA0W_thumb,.-DaA0W_horizontalLayout.-DaA0W_hideLyric .-DaA0W_cover,.-DaA0W_horizontalLayout.-DaA0W_hideLyric .-DaA0W_controls{left:var(--hide-lyric-left)}@media screen and (max-height:1000px){.-DaA0W_horizontalLayout{--horizontal-layout-max-width: min(45vh, 38vw)}}@media screen and (max-height:768px){.-DaA0W_horizontalLayout{grid-template-rows:[-DaA0W_drag-area] minmax(30px,.45fr)[-DaA0W_thumb] auto[-DaA0W_cover] auto[-DaA0W_music-info] 3fr[-DaA0W_buttom-controls] 0fr .2fr;gap:2px;font-size:.8em}}.SqBoeq_verticalLayout{grid-template-rows:[SqBoeq_drag-area] 30px[SqBoeq_thumb] 30px[SqBoeq_main-view] 1fr[SqBoeq_control] 0fr;gap:2px;width:100%;min-width:0;height:100%;min-height:0;display:grid;position:relative;overflow:hidden}.SqBoeq_thumb{z-index:1;mix-blend-mode:plus-lighter;grid-row:SqBoeq_thumb;justify-self:center}@media screen and (max-width:768px){.SqBoeq_thumb{margin:4px}}.SqBoeq_coverFrame{width:0;height:0;position:absolute}.SqBoeq_coverFrame.SqBoeq_immerseCover{-webkit-mask-image:linear-gradient(#000 0% 70%,#0000);mask-image:linear-gradient(#000 0% 70%,#0000)}.SqBoeq_cover{aspect-ratio:1;z-index:1;position:absolute;top:0;left:0}.SqBoeq_lyricLayout{mix-blend-mode:plus-lighter;grid-area:SqBoeq_main-view / 1 / SqBoeq_main-view / 2;grid-template-rows:1em[SqBoeq_controls] 0fr[SqBoeq_lyric-view] 1fr[SqBoeq_bottom-controls] 0fr;grid-template-columns:3em[SqBoeq_cover-side] 0fr[SqBoeq_info-side] 1fr 3em;display:grid}@media screen and (max-width:480px){.SqBoeq_lyricLayout{grid-template-columns:20px[SqBoeq_cover-side] 0fr[SqBoeq_info-side] 1fr 20px}}.SqBoeq_noLyricLayout{pointer-events:none;grid-area:SqBoeq_main-view / 1 / SqBoeq_main-view / 2;grid-template-rows:1em[SqBoeq_cover-view] 1fr[SqBoeq_lyric-view] 0fr;grid-template-columns:3em[SqBoeq_main-view] 1fr 3em;display:grid;overflow-y:hidden}@media screen and (max-width:480px){.SqBoeq_noLyricLayout{grid-template-columns:2em[SqBoeq_main-view] 1fr 2em}}.SqBoeq_phonySmallCover{aspect-ratio:1;opacity:0;pointer-events:none;grid-area:SqBoeq_controls / SqBoeq_cover-side;place-self:center;width:6em}@media screen and (max-width:480px){.SqBoeq_phonySmallCover{width:4.5em}}.SqBoeq_phonyBigCover{opacity:0;pointer-events:none;grid-area:SqBoeq_cover-view / 2 / SqBoeq_cover-view / 3}.SqBoeq_smallControls{z-index:1;mix-blend-mode:plus-lighter;grid-area:SqBoeq_controls / SqBoeq_info-side;align-self:center;min-width:0;height:fit-content;padding-left:1em;transition:opacity .25s .25s;overflow:visible}.SqBoeq_lyric{opacity:1;mix-blend-mode:plus-lighter;box-sizing:content-box;grid-area:SqBoeq_lyric-view / 1 / SqBoeq_lyric-view / 5;min-height:0;transition:opacity .5s .5s;-webkit-mask-image:linear-gradient(#0000,#000 10% 100%);mask-image:linear-gradient(#0000,#000 10% 100%)}.SqBoeq_bigControls{opacity:0;mix-blend-mode:plus-lighter;z-index:2;grid-area:SqBoeq_lyric-view / 2 / SqBoeq_lyric-view / 3;min-width:0;transition:opacity .5s}.SqBoeq_hideLyric .SqBoeq_smallControls{opacity:0;pointer-events:none;transition:opacity .5s}.SqBoeq_hideLyric .SqBoeq_noLyricLayout{pointer-events:auto}.SqBoeq_hideLyric .SqBoeq_lyric{opacity:0;pointer-events:none;transition:opacity .5s}.SqBoeq_hideLyric .SqBoeq_bigControls{opacity:1}.SqBoeq_background{width:100%;height:100%;position:absolute}._6-mjea_toggleIconButton{aspect-ratio:1;color:currentColor;opacity:.45;background-color:#0000;border:none;justify-content:center;align-items:center;width:4em;height:4em;display:flex}._6-mjea_toggleIconButton svg{aspect-ratio:1;width:4em;height:4em}@media screen and (max-width:1600px),(max-height:1000px){._6-mjea_toggleIconButton{width:3em;height:3em}._6-mjea_toggleIconButton svg{width:3em;height:3em}}@keyframes LJEMWq_speaker-bounce-part1{to{transform:scale(1.2)}}@keyframes LJEMWq_speaker-bounce-part2{to{transform:scale(.85)}}@keyframes LJEMWq_speaker-bounce-part3{to{transform:scale(1)}}.LJEMWq_volumeControl{flex-direction:row;align-items:center;margin-left:-12px;margin-right:-8px;display:flex}.LJEMWq_volumeControl>svg{opacity:.5}.LJEMWq_speakerAnimate .speaker-bounce-1{transform-origin:center;animation:.2s ease-in-out forwards LJEMWq_speaker-bounce-part1,.2s ease-in-out .2s forwards LJEMWq_speaker-bounce-part2,.2s ease-in-out .4s forwards LJEMWq_speaker-bounce-part3}.LJEMWq_speakerAnimate .speaker-bounce-2{transform-origin:center;animation:.2s ease-in-out 50ms forwards LJEMWq_speaker-bounce-part1,.2s ease-in-out .25s forwards LJEMWq_speaker-bounce-part2,.2s ease-in-out .45s forwards LJEMWq_speaker-bounce-part3}.LJEMWq_speakerAnimate .speaker-bounce-3{transform-origin:center;animation:.2s ease-in-out .1s forwards LJEMWq_speaker-bounce-part1,.2s ease-in-out .3s forwards LJEMWq_speaker-bounce-part2,.2s ease-in-out .5s forwards LJEMWq_speaker-bounce-part3}.LJEMWq_speakerAnimate .speaker-bounce-4{transform-origin:center;animation:.2s ease-in-out .15s forwards LJEMWq_speaker-bounce-part1,.2s ease-in-out .35s forwards LJEMWq_speaker-bounce-part2,.2s ease-in-out .55s forwards LJEMWq_speaker-bounce-part3}.amll-forward-left-arrow{animation:.6s cubic-bezier(.4,1.51,.4,1) forwards amll-forward-left-end}.amll-forward-right-arrow{transform-origin:81%;animation:.2s ease-out forwards amll-forward-right-end}.amll-forward-left-standby{transform-origin:22%;animation:.6s cubic-bezier(.4,1.51,.4,1) forwards amll-forward-left-new}@keyframes amll-forward-left-end{to{transform:translate(40px)}}@keyframes amll-forward-right-end{to{opacity:0;transform:scale(0)}}@keyframes amll-forward-left-new{0%{opacity:0;transform:scale(0) translate(-15px)}to{opacity:1;transform:scale(1) translate(0)}}.amll-rewind-right-arrow{animation:.6s cubic-bezier(.4,1.51,.4,1) forwards amll-rewind-right-end}.amll-rewind-left-arrow{transform-origin:19%;animation:.2s ease-out forwards amll-rewind-left-end}.amll-rewind-right-standby{transform-origin:78%;animation:.6s cubic-bezier(.4,1.51,.4,1) forwards amll-rewind-right-new}@keyframes amll-rewind-right-end{to{transform:translate(-40px)}}@keyframes amll-rewind-left-end{to{opacity:0;transform:scale(0)}}@keyframes amll-rewind-right-new{0%{opacity:0;transform:scale(0) translate(15px)}to{opacity:1;transform:scale(1) translate(0)}}.UjzbJG_horizontalControls{min-width:0;overflow:hidden}@media screen and (max-height:1000px){.UjzbJG_horizontalControls.UjzbJG_horizontalControls{padding:0}}@media screen and (min-height:1000px){.UjzbJG_horizontalControls{padding-top:2vh;padding-bottom:1vh}}.UjzbJG_controls,.UjzbJG_bigControls{justify-content:space-evenly;align-items:center;display:flex}.UjzbJG_controls{justify-content:space-between}.UjzbJG_bigControls{padding:2vh min(2vw,2vh) 0}.UjzbJG_songMediaButton,.UjzbJG_songMediaPlayButton{aspect-ratio:1;width:18%}.UjzbJG_songMediaButton>svg{transition:scale .3s;scale:3}@media screen and (max-height:1080px){.UjzbJG_songMediaButton>svg{scale:2}}@media screen and (max-height:768px){.UjzbJG_songMediaButton>svg{scale:1.5}}@media screen and (max-height:512px){.UjzbJG_songMediaButton>svg{scale:1}}@media screen and (max-width:480px){.UjzbJG_songMediaButton>svg{scale:.5}}.UjzbJG_songMediaPlayButton>svg{transition:scale .3s;scale:2}@media screen and (max-height:1080px){.UjzbJG_songMediaPlayButton>svg{scale:1.1}}@media screen and (max-height:768px){.UjzbJG_songMediaPlayButton>svg{scale:.8}}@media screen and (max-height:512px){.UjzbJG_songMediaPlayButton>svg{scale:.5}}@media screen and (max-width:480px){.UjzbJG_songMediaPlayButton>svg{scale:.5}}.UjzbJG_qualityTag{transition:scale .3s;scale:1.4}@media screen and (max-height:768px){.UjzbJG_qualityTag{scale:.5}}@media screen and (max-height:1080px){.UjzbJG_qualityTag{scale:1}}.UjzbJG_progressBarLabels{opacity:.5;justify-content:space-between;align-items:center;margin-top:4px;font-size:max(1.2vh,.8em);font-weight:500;display:flex}@media screen and (max-height:768px){.UjzbJG_progressBarLabels{margin-top:0}}.UjzbJG_progressBarLabels>*{flex:1}.UjzbJG_progressBarLabels>:nth-child(2){flex:0}.UjzbJG_progressBarLabels>:last-child{text-align:right}.UjzbJG_bigControls>button{width:10vh;height:10vh}.UjzbJG_bigControls>button:nth-child(2)>svg{scale:1.75}@media screen and (max-width:480px){.UjzbJG_bigControls>button:nth-child(2)>svg{scale:1.25}}.UjzbJG_bigControls>button>svg{scale:2.5}@media screen and (max-width:480px){.UjzbJG_bigControls>button>svg{scale:2}}.UjzbJG_autoLyricLayout{--info-timing-func-in: cubic-bezier(.5, 0, .75, 0);--info-timing-func-out: cubic-bezier(.25, 1, .5, 1)}.UjzbJG_bigMusicInfo{transition:transform .3s var(--info-timing-func-in),opacity .3s var(--info-timing-func-in);opacity:0;min-width:0;padding:2em 0;overflow-x:hidden;transform:translateY(-25vh)}@media screen and (max-height:768px){.UjzbJG_bigMusicInfo{padding:1em 0}}@media screen and (max-height:512px){.UjzbJG_bigMusicInfo{padding:.25em 0}}.UjzbJG_bigMusicInfo.UjzbJG_hideLyric{transition:transform .5s .3s var(--info-timing-func-out),opacity .5s .3s var(--info-timing-func-out);opacity:1;transform:translateY(0)}.UjzbJG_smallMusicInfo{transition:transform .5s .3s var(--info-timing-func-out),opacity .3s .3s var(--info-timing-func-out);opacity:1;font-size:clamp(1em,2vh,1.25em);transform:translateY(0)}.UjzbJG_smallMusicInfo.UjzbJG_hideLyric{transition:transform .3s var(--info-timing-func-in),opacity .3s var(--info-timing-func-in);opacity:0;transform:translateY(25vh)}.UjzbJG_bigVolumeControl{padding-bottom:4em}@media screen and (max-height:768px){.UjzbJG_bigVolumeControl{padding-bottom:2em}}@media screen and (max-height:512px){.UjzbJG_bigVolumeControl{padding-bottom:1em}}html,body,#root{height:100%;margin:0;padding:0;background:#0b0b0f;color:#fff;overflow:hidden;-webkit-tap-highlight-color:transparent}body{font-family:Noto Sans SC,-apple-system,PingFang SC,system-ui,sans-serif}.stage{position:fixed;inset:0;display:flex;align-items:stretch;justify-content:center}.stage>#amll-lyric-player{position:relative;z-index:1}#amll-lyric-player{box-sizing:border-box;padding:0}#amll-lyric-player [class*=horizontalLayout],#amll-lyric-player [class*=verticalLayout]{box-sizing:border-box;padding:calc(var(--amll-inset-top, 0px) + 8px) 8px var(--amll-inset-bottom, 0px)}@media(orientation:portrait){#amll-lyric-player .amll-lyric-player{font-size:max(9.5vw,30px)!important}}@media(orientation:landscape)and (max-height:640px){#amll-lyric-player [class*=horizontalLayout]{--horizontal-layout-max-width: min(36vh, 34vw, calc(100vh - 240px) )}#amll-lyric-player [class*=musicInfo],#amll-lyric-player [class*=horizontalControls],#amll-lyric-player [class*=controls],#amll-lyric-player [class*=volumeControl]{flex-shrink:0}}#amll-lyric-player ::-webkit-scrollbar{display:none}.stage{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}.menu-backdrop{position:fixed;inset:0;z-index:1000}.menu-card{position:fixed;box-sizing:border-box;padding:6px;border-radius:12px;background:#fff;color:#1c1c1e;box-shadow:0 14px 40px -12px #0009;display:flex;flex-direction:column}.menu-item{appearance:none;border:0;background:transparent;color:inherit;font-family:inherit;font-size:14px;line-height:1.2;text-align:left;padding:10px 12px;border-radius:6px;cursor:default}.menu-item:active{background:#ededf0}.menu-item:disabled{color:#b0b0b6}.menu-sep{height:1px;margin:4px 8px;background:#0000001a}.page-error{position:fixed;inset:0;padding:16px;overflow:auto;box-sizing:border-box;background:#300018;color:#fff;font-size:12px;white-space:pre-wrap}.page-error-title{color:#ffb4ab;font-weight:600;margin-bottom:8px}.page-error pre{margin:0;font-size:11px;white-space:pre-wrap;word-break:break-all} diff --git a/android/app/src/main/assets/lyrics/assets/index-znGokPYv.js b/android/app/src/main/assets/lyrics/assets/index-znGokPYv.js new file mode 100644 index 0000000..cb062a9 --- /dev/null +++ b/android/app/src/main/assets/lyrics/assets/index-znGokPYv.js @@ -0,0 +1,819 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const c of o.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&i(c)}).observe(document,{childList:!0,subtree:!0});function e(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(a){if(a.ep)return;a.ep=!0;const o=e(a);fetch(a.href,o)}})();var Zu=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function jm(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}function JT(n){if(Object.prototype.hasOwnProperty.call(n,"__esModule"))return n;var t=n.default;if(typeof t=="function"){var e=function i(){var a=!1;try{a=this instanceof i}catch{}return a?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};e.prototype=t.prototype}else e={};return Object.defineProperty(e,"__esModule",{value:!0}),Object.keys(n).forEach(function(i){var a=Object.getOwnPropertyDescriptor(n,i);Object.defineProperty(e,i,a.get?a:{enumerable:!0,get:function(){return n[i]}})}),e}var $1={exports:{}},Qo={};var Wg;function tC(){if(Wg)return Qo;Wg=1;var n=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function e(i,a,o){var c=null;if(o!==void 0&&(c=""+o),a.key!==void 0&&(c=""+a.key),"key"in a){o={};for(var u in a)u!=="key"&&(o[u]=a[u])}else o=a;return a=o.ref,{$$typeof:n,type:i,key:c,ref:a!==void 0?a:null,props:o}}return Qo.Fragment=t,Qo.jsx=e,Qo.jsxs=e,Qo}var Zg;function eC(){return Zg||(Zg=1,$1.exports=tC()),$1.exports}var Y=eC(),W1={exports:{}},Dt={};var Kg;function nC(){if(Kg)return Dt;Kg=1;var n=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),e=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),a=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),c=Symbol.for("react.context"),u=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),m=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),v=Symbol.iterator;function x(M){return M===null||typeof M!="object"?null:(M=v&&M[v]||M["@@iterator"],typeof M=="function"?M:null)}var b={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,T={};function S(M,z,j){this.props=M,this.context=z,this.refs=T,this.updater=j||b}S.prototype.isReactComponent={},S.prototype.setState=function(M,z){if(typeof M!="object"&&typeof M!="function"&&M!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,M,z,"setState")},S.prototype.forceUpdate=function(M){this.updater.enqueueForceUpdate(this,M,"forceUpdate")};function L(){}L.prototype=S.prototype;function R(M,z,j){this.props=M,this.context=z,this.refs=T,this.updater=j||b}var P=R.prototype=new L;P.constructor=R,C(P,S.prototype),P.isPureReactComponent=!0;var w=Array.isArray;function F(){}var E={H:null,A:null,T:null,S:null},O=Object.prototype.hasOwnProperty;function I(M,z,j){var W=j.ref;return{$$typeof:n,type:M,key:z,ref:W!==void 0?W:null,props:j}}function k(M,z){return I(M.type,z,M.props)}function U(M){return typeof M=="object"&&M!==null&&M.$$typeof===n}function G(M){var z={"=":"=0",":":"=2"};return"$"+M.replace(/[=:]/g,function(j){return z[j]})}var J=/\/+/g;function tt(M,z){return typeof M=="object"&&M!==null&&M.key!=null?G(""+M.key):z.toString(36)}function K(M){switch(M.status){case"fulfilled":return M.value;case"rejected":throw M.reason;default:switch(typeof M.status=="string"?M.then(F,F):(M.status="pending",M.then(function(z){M.status==="pending"&&(M.status="fulfilled",M.value=z)},function(z){M.status==="pending"&&(M.status="rejected",M.reason=z)})),M.status){case"fulfilled":return M.value;case"rejected":throw M.reason}}throw M}function A(M,z,j,W,st){var ct=typeof M;(ct==="undefined"||ct==="boolean")&&(M=null);var ft=!1;if(M===null)ft=!0;else switch(ct){case"bigint":case"string":case"number":ft=!0;break;case"object":switch(M.$$typeof){case n:case t:ft=!0;break;case m:return ft=M._init,A(ft(M._payload),z,j,W,st)}}if(ft)return st=st(M),ft=W===""?"."+tt(M,0):W,w(st)?(j="",ft!=null&&(j=ft.replace(J,"$&/")+"/"),A(st,z,j,"",function(dt){return dt})):st!=null&&(U(st)&&(st=k(st,j+(st.key==null||M&&M.key===st.key?"":(""+st.key).replace(J,"$&/")+"/")+ft)),z.push(st)),1;ft=0;var pt=W===""?".":W+":";if(w(M))for(var mt=0;mt>>1,V=A[H];if(0>>1;Ha(j,N))Wa(st,j)?(A[H]=st,A[W]=N,H=W):(A[H]=j,A[z]=N,H=z);else if(Wa(st,N))A[H]=st,A[W]=N,H=W;else break t}}return B}function a(A,B){var N=A.sortIndex-B.sortIndex;return N!==0?N:A.id-B.id}if(n.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;n.unstable_now=function(){return o.now()}}else{var c=Date,u=c.now();n.unstable_now=function(){return c.now()-u}}var f=[],d=[],m=1,g=null,v=3,x=!1,b=!1,C=!1,T=!1,S=typeof setTimeout=="function"?setTimeout:null,L=typeof clearTimeout=="function"?clearTimeout:null,R=typeof setImmediate<"u"?setImmediate:null;function P(A){for(var B=e(d);B!==null;){if(B.callback===null)i(d);else if(B.startTime<=A)i(d),B.sortIndex=B.expirationTime,t(f,B);else break;B=e(d)}}function w(A){if(C=!1,P(A),!b)if(e(f)!==null)b=!0,F||(F=!0,G());else{var B=e(d);B!==null&&K(w,B.startTime-A)}}var F=!1,E=-1,O=5,I=-1;function k(){return T?!0:!(n.unstable_now()-IA&&k());){var H=g.callback;if(typeof H=="function"){g.callback=null,v=g.priorityLevel;var V=H(g.expirationTime<=A);if(A=n.unstable_now(),typeof V=="function"){g.callback=V,P(A),B=!0;break e}g===e(f)&&i(f),P(A)}else i(f);g=e(f)}if(g!==null)B=!0;else{var M=e(d);M!==null&&K(w,M.startTime-A),B=!1}}break t}finally{g=null,v=N,x=!1}B=void 0}}finally{B?G():F=!1}}}var G;if(typeof R=="function")G=function(){R(U)};else if(typeof MessageChannel<"u"){var J=new MessageChannel,tt=J.port2;J.port1.onmessage=U,G=function(){tt.postMessage(null)}}else G=function(){S(U,0)};function K(A,B){E=S(function(){A(n.unstable_now())},B)}n.unstable_IdlePriority=5,n.unstable_ImmediatePriority=1,n.unstable_LowPriority=4,n.unstable_NormalPriority=3,n.unstable_Profiling=null,n.unstable_UserBlockingPriority=2,n.unstable_cancelCallback=function(A){A.callback=null},n.unstable_forceFrameRate=function(A){0>A||125H?(A.sortIndex=N,t(d,A),e(f)===null&&A===e(d)&&(C?(L(E),E=-1):C=!0,K(w,N-H))):(A.sortIndex=V,t(f,A),b||x||(b=!0,F||(F=!0,G()))),A},n.unstable_shouldYield=k,n.unstable_wrapCallback=function(A){var B=v;return function(){var N=v;v=B;try{return A.apply(this,arguments)}finally{v=N}}}})(Q1)),Q1}var ty;function sC(){return ty||(ty=1,K1.exports=iC()),K1.exports}var J1={exports:{}},rn={};var ey;function rC(){if(ey)return rn;ey=1;var n=vh();function t(f){var d="https://react.dev/errors/"+f;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(t){console.error(t)}}return n(),J1.exports=rC(),J1.exports}var iy;function aC(){if(iy)return Jo;iy=1;var n=sC(),t=vh(),e=q8();function i(s){var r="https://react.dev/errors/"+s;if(1V||(s.current=H[V],H[V]=null,V--)}function j(s,r){V++,H[V]=s.current,s.current=r}var W=M(null),st=M(null),ct=M(null),ft=M(null);function pt(s,r){switch(j(ct,r),j(st,s),j(W,null),r.nodeType){case 9:case 11:s=(s=r.documentElement)&&(s=s.namespaceURI)?vg(s):0;break;default:if(s=r.tagName,r=r.namespaceURI)r=vg(r),s=bg(r,s);else switch(s){case"svg":s=1;break;case"math":s=2;break;default:s=0}}z(W),j(W,s)}function mt(){z(W),z(st),z(ct)}function dt(s){s.memoizedState!==null&&j(ft,s);var r=W.current,l=bg(r,s.type);r!==l&&(j(st,s),j(W,l))}function St(s){st.current===s&&(z(W),z(st)),ft.current===s&&(z(ft),$o._currentValue=N)}var bt,Lt;function wt(s){if(bt===void 0)try{throw Error()}catch(l){var r=l.stack.trim().match(/\n( *(at )?)/);bt=r&&r[1]||"",Lt=-1)":-1p||X[h]!==nt[p]){var lt=` +`+X[h].replace(" at new "," at ");return s.displayName&<.includes("")&&(lt=lt.replace("",s.displayName)),lt}while(1<=h&&0<=p);break}}}finally{yt=!1,Error.prepareStackTrace=l}return(l=s?s.displayName||s.name:"")?wt(l):""}function ie(s,r){switch(s.tag){case 26:case 27:case 5:return wt(s.type);case 16:return wt("Lazy");case 13:return s.child!==r&&r!==null?wt("Suspense Fallback"):wt("Suspense");case 19:return wt("SuspenseList");case 0:case 15:return xt(s.type,!1);case 11:return xt(s.type.render,!1);case 1:return xt(s.type,!0);case 31:return wt("Activity");default:return""}}function De(s){try{var r="",l=null;do r+=ie(s,l),l=s,s=s.return;while(s);return r}catch(h){return` +Error generating stack: `+h.message+` +`+h.stack}}var Sn=Object.prototype.hasOwnProperty,Dn=n.unstable_scheduleCallback,ln=n.unstable_cancelCallback,ss=n.unstable_shouldYield,rs=n.unstable_requestPaint,Be=n.unstable_now,Fh=n.unstable_getCurrentPriorityLevel,Zl=n.unstable_ImmediatePriority,io=n.unstable_UserBlockingPriority,Ur=n.unstable_NormalPriority,Hs=n.unstable_LowPriority,kr=n.unstable_IdlePriority,Kl=n.log,Uh=n.unstable_setDisableYieldValue,qs=null,Ze=null;function _n(s){if(typeof Kl=="function"&&Uh(s),Ze&&typeof Ze.setStrictMode=="function")try{Ze.setStrictMode(qs,s)}catch{}}var Q=Math.clz32?Math.clz32:fe,rt=Math.log,Qt=Math.LN2;function fe(s){return s>>>=0,s===0?32:31-(rt(s)/Qt|0)|0}var oe=256,It=262144,An=4194304;function cn(s){var r=s&42;if(r!==0)return r;switch(s&-s){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return s&261888;case 262144:case 524288:case 1048576:case 2097152:return s&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return s&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return s}}function Xe(s,r,l){var h=s.pendingLanes;if(h===0)return 0;var p=0,y=s.suspendedLanes,_=s.pingedLanes;s=s.warmLanes;var D=h&134217727;return D!==0?(h=D&~y,h!==0?p=cn(h):(_&=D,_!==0?p=cn(_):l||(l=D&~s,l!==0&&(p=cn(l))))):(D=h&~y,D!==0?p=cn(D):_!==0?p=cn(_):l||(l=h&~s,l!==0&&(p=cn(l)))),p===0?0:r!==0&&r!==p&&(r&y)===0&&(y=p&-p,l=r&-r,y>=l||y===32&&(l&4194048)!==0)?r:p}function pn(s,r){return(s.pendingLanes&~(s.suspendedLanes&~s.pingedLanes)&r)===0}function so(s,r){switch(s){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ri(){var s=An;return An<<=1,(An&62914560)===0&&(An=4194304),s}function un(s){for(var r=[],l=0;31>l;l++)r.push(s);return r}function as(s,r){s.pendingLanes|=r,r!==268435456&&(s.suspendedLanes=0,s.pingedLanes=0,s.warmLanes=0)}function Ql(s,r,l,h,p,y){var _=s.pendingLanes;s.pendingLanes=l,s.suspendedLanes=0,s.pingedLanes=0,s.warmLanes=0,s.expiredLanes&=l,s.entangledLanes&=l,s.errorRecoveryDisabledLanes&=l,s.shellSuspendCounter=0;var D=s.entanglements,X=s.expirationTimes,nt=s.hiddenUpdates;for(l=_&~l;0"u")return null;try{return s.activeElement||s.body}catch{return s.body}}var Yb=/[\n"\\]/g;function Pn(s){return s.replace(Yb,function(r){return"\\"+r.charCodeAt(0).toString(16)+" "})}function Vh(s,r,l,h,p,y,_,D){s.name="",_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"?s.type=_:s.removeAttribute("type"),r!=null?_==="number"?(r===0&&s.value===""||s.value!=r)&&(s.value=""+Bn(r)):s.value!==""+Bn(r)&&(s.value=""+Bn(r)):_!=="submit"&&_!=="reset"||s.removeAttribute("value"),r!=null?Gh(s,_,Bn(r)):l!=null?Gh(s,_,Bn(l)):h!=null&&s.removeAttribute("value"),p==null&&y!=null&&(s.defaultChecked=!!y),p!=null&&(s.checked=p&&typeof p!="function"&&typeof p!="symbol"),D!=null&&typeof D!="function"&&typeof D!="symbol"&&typeof D!="boolean"?s.name=""+Bn(D):s.removeAttribute("name")}function f3(s,r,l,h,p,y,_,D){if(y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"&&(s.type=y),r!=null||l!=null){if(!(y!=="submit"&&y!=="reset"||r!=null)){zh(s);return}l=l!=null?""+Bn(l):"",r=r!=null?""+Bn(r):l,D||r===s.value||(s.value=r),s.defaultValue=r}h=h??p,h=typeof h!="function"&&typeof h!="symbol"&&!!h,s.checked=D?s.checked:!!h,s.defaultChecked=!!h,_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"&&(s.name=_),zh(s)}function Gh(s,r,l){r==="number"&&ec(s.ownerDocument)===s||s.defaultValue===""+l||(s.defaultValue=""+l)}function Xr(s,r,l,h){if(s=s.options,r){r={};for(var p=0;p"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Yh=!1;if(Di)try{var ho={};Object.defineProperty(ho,"passive",{get:function(){Yh=!0}}),window.addEventListener("test",ho,ho),window.removeEventListener("test",ho,ho)}catch{Yh=!1}var cs=null,$h=null,ic=null;function b3(){if(ic)return ic;var s,r=$h,l=r.length,h,p="value"in cs?cs.value:cs.textContent,y=p.length;for(s=0;s=po),A3=" ",E3=!1;function w3(s,r){switch(s){case"keyup":return Tx.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function M3(s){return s=s.detail,typeof s=="object"&&"data"in s?s.data:null}var Zr=!1;function Sx(s,r){switch(s){case"compositionend":return M3(r);case"keypress":return r.which!==32?null:(E3=!0,A3);case"textInput":return s=r.data,s===A3&&E3?null:s;default:return null}}function _x(s,r){if(Zr)return s==="compositionend"||!Jh&&w3(s,r)?(s=b3(),ic=$h=cs=null,Zr=!1,s):null;switch(s){case"paste":return null;case"keypress":if(!(r.ctrlKey||r.altKey||r.metaKey)||r.ctrlKey&&r.altKey){if(r.char&&1=r)return{node:l,offset:r-s};s=h}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=P3(l)}}function U3(s,r){return s&&r?s===r?!0:s&&s.nodeType===3?!1:r&&r.nodeType===3?U3(s,r.parentNode):"contains"in s?s.contains(r):s.compareDocumentPosition?!!(s.compareDocumentPosition(r)&16):!1:!1}function k3(s){s=s!=null&&s.ownerDocument!=null&&s.ownerDocument.defaultView!=null?s.ownerDocument.defaultView:window;for(var r=ec(s.document);r instanceof s.HTMLIFrameElement;){try{var l=typeof r.contentWindow.location.href=="string"}catch{l=!1}if(l)s=r.contentWindow;else break;r=ec(s.document)}return r}function nf(s){var r=s&&s.nodeName&&s.nodeName.toLowerCase();return r&&(r==="input"&&(s.type==="text"||s.type==="search"||s.type==="tel"||s.type==="url"||s.type==="password")||r==="textarea"||s.contentEditable==="true")}var Ox=Di&&"documentMode"in document&&11>=document.documentMode,Kr=null,sf=null,bo=null,rf=!1;function z3(s,r,l){var h=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;rf||Kr==null||Kr!==ec(h)||(h=Kr,"selectionStart"in h&&nf(h)?h={start:h.selectionStart,end:h.selectionEnd}:(h=(h.ownerDocument&&h.ownerDocument.defaultView||window).getSelection(),h={anchorNode:h.anchorNode,anchorOffset:h.anchorOffset,focusNode:h.focusNode,focusOffset:h.focusOffset}),bo&&vo(bo,h)||(bo=h,h=Zc(sf,"onSelect"),0>=_,p-=_,mi=1<<32-Q(r)+p|l<Pt?(Xt=At,At=null):Xt=At.sibling;var ee=it(Z,At,et[Pt],ut);if(ee===null){At===null&&(At=Xt);break}s&&At&&ee.alternate===null&&r(Z,At),$=y(ee,$,Pt),te===null?Et=ee:te.sibling=ee,te=ee,At=Xt}if(Pt===et.length)return l(Z,At),Yt&&Pi(Z,Pt),Et;if(At===null){for(;PtPt?(Xt=At,At=null):Xt=At.sibling;var Ns=it(Z,At,ee.value,ut);if(Ns===null){At===null&&(At=Xt);break}s&&At&&Ns.alternate===null&&r(Z,At),$=y(Ns,$,Pt),te===null?Et=Ns:te.sibling=Ns,te=Ns,At=Xt}if(ee.done)return l(Z,At),Yt&&Pi(Z,Pt),Et;if(At===null){for(;!ee.done;Pt++,ee=et.next())ee=ht(Z,ee.value,ut),ee!==null&&($=y(ee,$,Pt),te===null?Et=ee:te.sibling=ee,te=ee);return Yt&&Pi(Z,Pt),Et}for(At=h(At);!ee.done;Pt++,ee=et.next())ee=at(At,Z,Pt,ee.value,ut),ee!==null&&(s&&ee.alternate!==null&&At.delete(ee.key===null?Pt:ee.key),$=y(ee,$,Pt),te===null?Et=ee:te.sibling=ee,te=ee);return s&&At.forEach(function(QT){return r(Z,QT)}),Yt&&Pi(Z,Pt),Et}function pe(Z,$,et,ut){if(typeof et=="object"&&et!==null&&et.type===C&&et.key===null&&(et=et.props.children),typeof et=="object"&&et!==null){switch(et.$$typeof){case x:t:{for(var Et=et.key;$!==null;){if($.key===Et){if(Et=et.type,Et===C){if($.tag===7){l(Z,$.sibling),ut=p($,et.props.children),ut.return=Z,Z=ut;break t}}else if($.elementType===Et||typeof Et=="object"&&Et!==null&&Et.$$typeof===O&&ir(Et)===$.type){l(Z,$.sibling),ut=p($,et.props),Ao(ut,et),ut.return=Z,Z=ut;break t}l(Z,$);break}else r(Z,$);$=$.sibling}et.type===C?(ut=Qs(et.props.children,Z.mode,ut,et.key),ut.return=Z,Z=ut):(ut=dc(et.type,et.key,et.props,null,Z.mode,ut),Ao(ut,et),ut.return=Z,Z=ut)}return _(Z);case b:t:{for(Et=et.key;$!==null;){if($.key===Et)if($.tag===4&&$.stateNode.containerInfo===et.containerInfo&&$.stateNode.implementation===et.implementation){l(Z,$.sibling),ut=p($,et.children||[]),ut.return=Z,Z=ut;break t}else{l(Z,$);break}else r(Z,$);$=$.sibling}ut=ff(et,Z.mode,ut),ut.return=Z,Z=ut}return _(Z);case O:return et=ir(et),pe(Z,$,et,ut)}if(K(et))return Ct(Z,$,et,ut);if(G(et)){if(Et=G(et),typeof Et!="function")throw Error(i(150));return et=Et.call(et),Mt(Z,$,et,ut)}if(typeof et.then=="function")return pe(Z,$,xc(et),ut);if(et.$$typeof===R)return pe(Z,$,gc(Z,et),ut);Tc(Z,et)}return typeof et=="string"&&et!==""||typeof et=="number"||typeof et=="bigint"?(et=""+et,$!==null&&$.tag===6?(l(Z,$.sibling),ut=p($,et),ut.return=Z,Z=ut):(l(Z,$),ut=hf(et,Z.mode,ut),ut.return=Z,Z=ut),_(Z)):l(Z,$)}return function(Z,$,et,ut){try{_o=0;var Et=pe(Z,$,et,ut);return la=null,Et}catch(At){if(At===oa||At===vc)throw At;var te=wn(29,At,null,Z.mode);return te.lanes=ut,te.return=Z,te}}}var rr=cp(!0),up=cp(!1),ms=!1;function _f(s){s.updateQueue={baseState:s.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Af(s,r){s=s.updateQueue,r.updateQueue===s&&(r.updateQueue={baseState:s.baseState,firstBaseUpdate:s.firstBaseUpdate,lastBaseUpdate:s.lastBaseUpdate,shared:s.shared,callbacks:null})}function ps(s){return{lane:s,tag:0,payload:null,callback:null,next:null}}function gs(s,r,l){var h=s.updateQueue;if(h===null)return null;if(h=h.shared,(se&2)!==0){var p=h.pending;return p===null?r.next=r:(r.next=p.next,p.next=r),h.pending=r,r=fc(s),Y3(s,null,l),r}return hc(s,h,r,l),fc(s)}function Eo(s,r,l){if(r=r.updateQueue,r!==null&&(r=r.shared,(l&4194048)!==0)){var h=r.lanes;h&=s.pendingLanes,l|=h,r.lanes=l,os(s,l)}}function Ef(s,r){var l=s.updateQueue,h=s.alternate;if(h!==null&&(h=h.updateQueue,l===h)){var p=null,y=null;if(l=l.firstBaseUpdate,l!==null){do{var _={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};y===null?p=y=_:y=y.next=_,l=l.next}while(l!==null);y===null?p=y=r:y=y.next=r}else p=y=r;l={baseState:h.baseState,firstBaseUpdate:p,lastBaseUpdate:y,shared:h.shared,callbacks:h.callbacks},s.updateQueue=l;return}s=l.lastBaseUpdate,s===null?l.firstBaseUpdate=r:s.next=r,l.lastBaseUpdate=r}var wf=!1;function wo(){if(wf){var s=aa;if(s!==null)throw s}}function Mo(s,r,l,h){wf=!1;var p=s.updateQueue;ms=!1;var y=p.firstBaseUpdate,_=p.lastBaseUpdate,D=p.shared.pending;if(D!==null){p.shared.pending=null;var X=D,nt=X.next;X.next=null,_===null?y=nt:_.next=nt,_=X;var lt=s.alternate;lt!==null&&(lt=lt.updateQueue,D=lt.lastBaseUpdate,D!==_&&(D===null?lt.firstBaseUpdate=nt:D.next=nt,lt.lastBaseUpdate=X))}if(y!==null){var ht=p.baseState;_=0,lt=nt=X=null,D=y;do{var it=D.lane&-536870913,at=it!==D.lane;if(at?(qt&it)===it:(h&it)===it){it!==0&&it===ra&&(wf=!0),lt!==null&&(lt=lt.next={lane:0,tag:D.tag,payload:D.payload,callback:null,next:null});t:{var Ct=s,Mt=D;it=r;var pe=l;switch(Mt.tag){case 1:if(Ct=Mt.payload,typeof Ct=="function"){ht=Ct.call(pe,ht,it);break t}ht=Ct;break t;case 3:Ct.flags=Ct.flags&-65537|128;case 0:if(Ct=Mt.payload,it=typeof Ct=="function"?Ct.call(pe,ht,it):Ct,it==null)break t;ht=g({},ht,it);break t;case 2:ms=!0}}it=D.callback,it!==null&&(s.flags|=64,at&&(s.flags|=8192),at=p.callbacks,at===null?p.callbacks=[it]:at.push(it))}else at={lane:it,tag:D.tag,payload:D.payload,callback:D.callback,next:null},lt===null?(nt=lt=at,X=ht):lt=lt.next=at,_|=it;if(D=D.next,D===null){if(D=p.shared.pending,D===null)break;at=D,D=at.next,at.next=null,p.lastBaseUpdate=at,p.shared.pending=null}}while(!0);lt===null&&(X=ht),p.baseState=X,p.firstBaseUpdate=nt,p.lastBaseUpdate=lt,y===null&&(p.shared.lanes=0),Ts|=_,s.lanes=_,s.memoizedState=ht}}function hp(s,r){if(typeof s!="function")throw Error(i(191,s));s.call(r)}function fp(s,r){var l=s.callbacks;if(l!==null)for(s.callbacks=null,s=0;sy?y:8;var _=A.T,D={};A.T=D,Xf(s,!1,r,l);try{var X=p(),nt=A.S;if(nt!==null&&nt(D,X),X!==null&&typeof X=="object"&&typeof X.then=="function"){var lt=Vx(X,h);No(s,r,lt,On(s))}else No(s,r,h,On(s))}catch(ht){No(s,r,{then:function(){},status:"rejected",reason:ht},On())}finally{B.p=y,_!==null&&D.types!==null&&(_.types=D.types),A.T=_}}function Yx(){}function Hf(s,r,l,h){if(s.tag!==5)throw Error(i(476));var p=Hp(s).queue;jp(s,p,r,N,l===null?Yx:function(){return qp(s),l(h)})}function Hp(s){var r=s.memoizedState;if(r!==null)return r;r={memoizedState:N,baseState:N,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:zi,lastRenderedState:N},next:null};var l={};return r.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:zi,lastRenderedState:l},next:null},s.memoizedState=r,s=s.alternate,s!==null&&(s.memoizedState=r),r}function qp(s){var r=Hp(s);r.next===null&&(r=s.alternate.memoizedState),No(s,r.next.queue,{},On())}function qf(){return Je($o)}function Xp(){return Oe().memoizedState}function Yp(){return Oe().memoizedState}function $x(s){for(var r=s.return;r!==null;){switch(r.tag){case 24:case 3:var l=On();s=ps(l);var h=gs(r,s,l);h!==null&&(Tn(h,r,l),Eo(h,r,l)),r={cache:xf()},s.payload=r;return}r=r.return}}function Wx(s,r,l){var h=On();l={lane:h,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Nc(s)?Wp(r,l):(l=cf(s,r,l,h),l!==null&&(Tn(l,s,h),Zp(l,r,h)))}function $p(s,r,l){var h=On();No(s,r,l,h)}function No(s,r,l,h){var p={lane:h,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(Nc(s))Wp(r,p);else{var y=s.alternate;if(s.lanes===0&&(y===null||y.lanes===0)&&(y=r.lastRenderedReducer,y!==null))try{var _=r.lastRenderedState,D=y(_,l);if(p.hasEagerState=!0,p.eagerState=D,En(D,_))return hc(s,r,p,0),ve===null&&uc(),!1}catch{}if(l=cf(s,r,p,h),l!==null)return Tn(l,s,h),Zp(l,r,h),!0}return!1}function Xf(s,r,l,h){if(h={lane:2,revertLane:_1(),gesture:null,action:h,hasEagerState:!1,eagerState:null,next:null},Nc(s)){if(r)throw Error(i(479))}else r=cf(s,l,h,2),r!==null&&Tn(r,s,2)}function Nc(s){var r=s.alternate;return s===Bt||r!==null&&r===Bt}function Wp(s,r){ua=_c=!0;var l=s.pending;l===null?r.next=r:(r.next=l.next,l.next=r),s.pending=r}function Zp(s,r,l){if((l&4194048)!==0){var h=r.lanes;h&=s.pendingLanes,l|=h,r.lanes=l,os(s,l)}}var Oo={readContext:Je,use:wc,useCallback:Ee,useContext:Ee,useEffect:Ee,useImperativeHandle:Ee,useLayoutEffect:Ee,useInsertionEffect:Ee,useMemo:Ee,useReducer:Ee,useRef:Ee,useState:Ee,useDebugValue:Ee,useDeferredValue:Ee,useTransition:Ee,useSyncExternalStore:Ee,useId:Ee,useHostTransitionStatus:Ee,useFormState:Ee,useActionState:Ee,useOptimistic:Ee,useMemoCache:Ee,useCacheRefresh:Ee};Oo.useEffectEvent=Ee;var Kp={readContext:Je,use:wc,useCallback:function(s,r){return hn().memoizedState=[s,r===void 0?null:r],s},useContext:Je,useEffect:Dp,useImperativeHandle:function(s,r,l){l=l!=null?l.concat([s]):null,Rc(4194308,4,Up.bind(null,r,s),l)},useLayoutEffect:function(s,r){return Rc(4194308,4,s,r)},useInsertionEffect:function(s,r){Rc(4,2,s,r)},useMemo:function(s,r){var l=hn();r=r===void 0?null:r;var h=s();if(ar){_n(!0);try{s()}finally{_n(!1)}}return l.memoizedState=[h,r],h},useReducer:function(s,r,l){var h=hn();if(l!==void 0){var p=l(r);if(ar){_n(!0);try{l(r)}finally{_n(!1)}}}else p=r;return h.memoizedState=h.baseState=p,s={pending:null,lanes:0,dispatch:null,lastRenderedReducer:s,lastRenderedState:p},h.queue=s,s=s.dispatch=Wx.bind(null,Bt,s),[h.memoizedState,s]},useRef:function(s){var r=hn();return s={current:s},r.memoizedState=s},useState:function(s){s=kf(s);var r=s.queue,l=$p.bind(null,Bt,r);return r.dispatch=l,[s.memoizedState,l]},useDebugValue:Gf,useDeferredValue:function(s,r){var l=hn();return jf(l,s,r)},useTransition:function(){var s=kf(!1);return s=jp.bind(null,Bt,s.queue,!0,!1),hn().memoizedState=s,[!1,s]},useSyncExternalStore:function(s,r,l){var h=Bt,p=hn();if(Yt){if(l===void 0)throw Error(i(407));l=l()}else{if(l=r(),ve===null)throw Error(i(349));(qt&127)!==0||vp(h,r,l)}p.memoizedState=l;var y={value:l,getSnapshot:r};return p.queue=y,Dp(xp.bind(null,h,y,s),[s]),h.flags|=2048,fa(9,{destroy:void 0},bp.bind(null,h,y,l,r),null),l},useId:function(){var s=hn(),r=ve.identifierPrefix;if(Yt){var l=pi,h=mi;l=(h&~(1<<32-Q(h)-1)).toString(32)+l,r="_"+r+"R_"+l,l=Ac++,0<\/script>",y=y.removeChild(y.firstChild);break;case"select":y=typeof h.is=="string"?_.createElement("select",{is:h.is}):_.createElement("select"),h.multiple?y.multiple=!0:h.size&&(y.size=h.size);break;default:y=typeof h.is=="string"?_.createElement(p,{is:h.is}):_.createElement(p)}}y[Le]=r,y[Ke]=h;t:for(_=r.child;_!==null;){if(_.tag===5||_.tag===6)y.appendChild(_.stateNode);else if(_.tag!==4&&_.tag!==27&&_.child!==null){_.child.return=_,_=_.child;continue}if(_===r)break t;for(;_.sibling===null;){if(_.return===null||_.return===r)break t;_=_.return}_.sibling.return=_.return,_=_.sibling}r.stateNode=y;t:switch(en(y,p,h),p){case"button":case"input":case"select":case"textarea":h=!!h.autoFocus;break t;case"img":h=!0;break t;default:h=!1}h&&Gi(r)}}return Ce(r),a1(r,r.type,s===null?null:s.memoizedProps,r.pendingProps,l),null;case 6:if(s&&r.stateNode!=null)s.memoizedProps!==h&&Gi(r);else{if(typeof h!="string"&&r.stateNode===null)throw Error(i(166));if(s=ct.current,ia(r)){if(s=r.stateNode,l=r.memoizedProps,h=null,p=Qe,p!==null)switch(p.tag){case 27:case 5:h=p.memoizedProps}s[Le]=r,s=!!(s.nodeValue===l||h!==null&&h.suppressHydrationWarning===!0||gg(s.nodeValue,l)),s||fs(r,!0)}else s=Kc(s).createTextNode(h),s[Le]=r,r.stateNode=s}return Ce(r),null;case 31:if(l=r.memoizedState,s===null||s.memoizedState!==null){if(h=ia(r),l!==null){if(s===null){if(!h)throw Error(i(318));if(s=r.memoizedState,s=s!==null?s.dehydrated:null,!s)throw Error(i(557));s[Le]=r}else Js(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Ce(r),s=!1}else l=gf(),s!==null&&s.memoizedState!==null&&(s.memoizedState.hydrationErrors=l),s=!0;if(!s)return r.flags&256?(Rn(r),r):(Rn(r),null);if((r.flags&128)!==0)throw Error(i(558))}return Ce(r),null;case 13:if(h=r.memoizedState,s===null||s.memoizedState!==null&&s.memoizedState.dehydrated!==null){if(p=ia(r),h!==null&&h.dehydrated!==null){if(s===null){if(!p)throw Error(i(318));if(p=r.memoizedState,p=p!==null?p.dehydrated:null,!p)throw Error(i(317));p[Le]=r}else Js(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Ce(r),p=!1}else p=gf(),s!==null&&s.memoizedState!==null&&(s.memoizedState.hydrationErrors=p),p=!0;if(!p)return r.flags&256?(Rn(r),r):(Rn(r),null)}return Rn(r),(r.flags&128)!==0?(r.lanes=l,r):(l=h!==null,s=s!==null&&s.memoizedState!==null,l&&(h=r.child,p=null,h.alternate!==null&&h.alternate.memoizedState!==null&&h.alternate.memoizedState.cachePool!==null&&(p=h.alternate.memoizedState.cachePool.pool),y=null,h.memoizedState!==null&&h.memoizedState.cachePool!==null&&(y=h.memoizedState.cachePool.pool),y!==p&&(h.flags|=2048)),l!==s&&l&&(r.child.flags|=8192),Pc(r,r.updateQueue),Ce(r),null);case 4:return mt(),s===null&&M1(r.stateNode.containerInfo),Ce(r),null;case 10:return Ui(r.type),Ce(r),null;case 19:if(z(Ne),h=r.memoizedState,h===null)return Ce(r),null;if(p=(r.flags&128)!==0,y=h.rendering,y===null)if(p)Do(h,!1);else{if(we!==0||s!==null&&(s.flags&128)!==0)for(s=r.child;s!==null;){if(y=Sc(s),y!==null){for(r.flags|=128,Do(h,!1),s=y.updateQueue,r.updateQueue=s,Pc(r,s),r.subtreeFlags=0,s=l,l=r.child;l!==null;)$3(l,s),l=l.sibling;return j(Ne,Ne.current&1|2),Yt&&Pi(r,h.treeForkCount),r.child}s=s.sibling}h.tail!==null&&Be()>Vc&&(r.flags|=128,p=!0,Do(h,!1),r.lanes=4194304)}else{if(!p)if(s=Sc(y),s!==null){if(r.flags|=128,p=!0,s=s.updateQueue,r.updateQueue=s,Pc(r,s),Do(h,!0),h.tail===null&&h.tailMode==="hidden"&&!y.alternate&&!Yt)return Ce(r),null}else 2*Be()-h.renderingStartTime>Vc&&l!==536870912&&(r.flags|=128,p=!0,Do(h,!1),r.lanes=4194304);h.isBackwards?(y.sibling=r.child,r.child=y):(s=h.last,s!==null?s.sibling=y:r.child=y,h.last=y)}return h.tail!==null?(s=h.tail,h.rendering=s,h.tail=s.sibling,h.renderingStartTime=Be(),s.sibling=null,l=Ne.current,j(Ne,p?l&1|2:l&1),Yt&&Pi(r,h.treeForkCount),s):(Ce(r),null);case 22:case 23:return Rn(r),Rf(),h=r.memoizedState!==null,s!==null?s.memoizedState!==null!==h&&(r.flags|=8192):h&&(r.flags|=8192),h?(l&536870912)!==0&&(r.flags&128)===0&&(Ce(r),r.subtreeFlags&6&&(r.flags|=8192)):Ce(r),l=r.updateQueue,l!==null&&Pc(r,l.retryQueue),l=null,s!==null&&s.memoizedState!==null&&s.memoizedState.cachePool!==null&&(l=s.memoizedState.cachePool.pool),h=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(h=r.memoizedState.cachePool.pool),h!==l&&(r.flags|=2048),s!==null&&z(nr),null;case 24:return l=null,s!==null&&(l=s.memoizedState.cache),r.memoizedState.cache!==l&&(r.flags|=2048),Ui(Pe),Ce(r),null;case 25:return null;case 30:return null}throw Error(i(156,r.tag))}function tT(s,r){switch(mf(r),r.tag){case 1:return s=r.flags,s&65536?(r.flags=s&-65537|128,r):null;case 3:return Ui(Pe),mt(),s=r.flags,(s&65536)!==0&&(s&128)===0?(r.flags=s&-65537|128,r):null;case 26:case 27:case 5:return St(r),null;case 31:if(r.memoizedState!==null){if(Rn(r),r.alternate===null)throw Error(i(340));Js()}return s=r.flags,s&65536?(r.flags=s&-65537|128,r):null;case 13:if(Rn(r),s=r.memoizedState,s!==null&&s.dehydrated!==null){if(r.alternate===null)throw Error(i(340));Js()}return s=r.flags,s&65536?(r.flags=s&-65537|128,r):null;case 19:return z(Ne),null;case 4:return mt(),null;case 10:return Ui(r.type),null;case 22:case 23:return Rn(r),Rf(),s!==null&&z(nr),s=r.flags,s&65536?(r.flags=s&-65537|128,r):null;case 24:return Ui(Pe),null;case 25:return null;default:return null}}function T4(s,r){switch(mf(r),r.tag){case 3:Ui(Pe),mt();break;case 26:case 27:case 5:St(r);break;case 4:mt();break;case 31:r.memoizedState!==null&&Rn(r);break;case 13:Rn(r);break;case 19:z(Ne);break;case 10:Ui(r.type);break;case 22:case 23:Rn(r),Rf(),s!==null&&z(nr);break;case 24:Ui(Pe)}}function Bo(s,r){try{var l=r.updateQueue,h=l!==null?l.lastEffect:null;if(h!==null){var p=h.next;l=p;do{if((l.tag&s)===s){h=void 0;var y=l.create,_=l.inst;h=y(),_.destroy=h}l=l.next}while(l!==p)}}catch(D){ue(r,r.return,D)}}function bs(s,r,l){try{var h=r.updateQueue,p=h!==null?h.lastEffect:null;if(p!==null){var y=p.next;h=y;do{if((h.tag&s)===s){var _=h.inst,D=_.destroy;if(D!==void 0){_.destroy=void 0,p=r;var X=l,nt=D;try{nt()}catch(lt){ue(p,X,lt)}}}h=h.next}while(h!==y)}}catch(lt){ue(r,r.return,lt)}}function C4(s){var r=s.updateQueue;if(r!==null){var l=s.stateNode;try{fp(r,l)}catch(h){ue(s,s.return,h)}}}function S4(s,r,l){l.props=or(s.type,s.memoizedProps),l.state=s.memoizedState;try{l.componentWillUnmount()}catch(h){ue(s,r,h)}}function Po(s,r){try{var l=s.ref;if(l!==null){switch(s.tag){case 26:case 27:case 5:var h=s.stateNode;break;case 30:h=s.stateNode;break;default:h=s.stateNode}typeof l=="function"?s.refCleanup=l(h):l.current=h}}catch(p){ue(s,r,p)}}function gi(s,r){var l=s.ref,h=s.refCleanup;if(l!==null)if(typeof h=="function")try{h()}catch(p){ue(s,r,p)}finally{s.refCleanup=null,s=s.alternate,s!=null&&(s.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(p){ue(s,r,p)}else l.current=null}function _4(s){var r=s.type,l=s.memoizedProps,h=s.stateNode;try{t:switch(r){case"button":case"input":case"select":case"textarea":l.autoFocus&&h.focus();break t;case"img":l.src?h.src=l.src:l.srcSet&&(h.srcset=l.srcSet)}}catch(p){ue(s,s.return,p)}}function o1(s,r,l){try{var h=s.stateNode;CT(h,s.type,l,r),h[Ke]=r}catch(p){ue(s,s.return,p)}}function A4(s){return s.tag===5||s.tag===3||s.tag===26||s.tag===27&&Es(s.type)||s.tag===4}function l1(s){t:for(;;){for(;s.sibling===null;){if(s.return===null||A4(s.return))return null;s=s.return}for(s.sibling.return=s.return,s=s.sibling;s.tag!==5&&s.tag!==6&&s.tag!==18;){if(s.tag===27&&Es(s.type)||s.flags&2||s.child===null||s.tag===4)continue t;s.child.return=s,s=s.child}if(!(s.flags&2))return s.stateNode}}function c1(s,r,l){var h=s.tag;if(h===5||h===6)s=s.stateNode,r?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(s,r):(r=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,r.appendChild(s),l=l._reactRootContainer,l!=null||r.onclick!==null||(r.onclick=Ii));else if(h!==4&&(h===27&&Es(s.type)&&(l=s.stateNode,r=null),s=s.child,s!==null))for(c1(s,r,l),s=s.sibling;s!==null;)c1(s,r,l),s=s.sibling}function Fc(s,r,l){var h=s.tag;if(h===5||h===6)s=s.stateNode,r?l.insertBefore(s,r):l.appendChild(s);else if(h!==4&&(h===27&&Es(s.type)&&(l=s.stateNode),s=s.child,s!==null))for(Fc(s,r,l),s=s.sibling;s!==null;)Fc(s,r,l),s=s.sibling}function E4(s){var r=s.stateNode,l=s.memoizedProps;try{for(var h=s.type,p=r.attributes;p.length;)r.removeAttributeNode(p[0]);en(r,h,l),r[Le]=s,r[Ke]=l}catch(y){ue(s,s.return,y)}}var ji=!1,ke=!1,u1=!1,w4=typeof WeakSet=="function"?WeakSet:Set,$e=null;function eT(s,r){if(s=s.containerInfo,N1=su,s=k3(s),nf(s)){if("selectionStart"in s)var l={start:s.selectionStart,end:s.selectionEnd};else t:{l=(l=s.ownerDocument)&&l.defaultView||window;var h=l.getSelection&&l.getSelection();if(h&&h.rangeCount!==0){l=h.anchorNode;var p=h.anchorOffset,y=h.focusNode;h=h.focusOffset;try{l.nodeType,y.nodeType}catch{l=null;break t}var _=0,D=-1,X=-1,nt=0,lt=0,ht=s,it=null;e:for(;;){for(var at;ht!==l||p!==0&&ht.nodeType!==3||(D=_+p),ht!==y||h!==0&&ht.nodeType!==3||(X=_+h),ht.nodeType===3&&(_+=ht.nodeValue.length),(at=ht.firstChild)!==null;)it=ht,ht=at;for(;;){if(ht===s)break e;if(it===l&&++nt===p&&(D=_),it===y&&++lt===h&&(X=_),(at=ht.nextSibling)!==null)break;ht=it,it=ht.parentNode}ht=at}l=D===-1||X===-1?null:{start:D,end:X}}else l=null}l=l||{start:0,end:0}}else l=null;for(O1={focusedElem:s,selectionRange:l},su=!1,$e=r;$e!==null;)if(r=$e,s=r.child,(r.subtreeFlags&1028)!==0&&s!==null)s.return=r,$e=s;else for(;$e!==null;){switch(r=$e,y=r.alternate,s=r.flags,r.tag){case 0:if((s&4)!==0&&(s=r.updateQueue,s=s!==null?s.events:null,s!==null))for(l=0;l title"))),en(y,h,l),y[Le]=s,Ye(y),h=y;break t;case"link":var _=Ig("link","href",p).get(h+(l.href||""));if(_){for(var D=0;D<_.length;D++)if(y=_[D],y.getAttribute("href")===(l.href==null||l.href===""?null:l.href)&&y.getAttribute("rel")===(l.rel==null?null:l.rel)&&y.getAttribute("title")===(l.title==null?null:l.title)&&y.getAttribute("crossorigin")===(l.crossOrigin==null?null:l.crossOrigin)){_.splice(D,1);break e}}y=p.createElement(h),en(y,h,l),p.head.appendChild(y);break;case"meta":if(_=Ig("meta","content",p).get(h+(l.content||""))){for(D=0;D<_.length;D++)if(y=_[D],y.getAttribute("content")===(l.content==null?null:""+l.content)&&y.getAttribute("name")===(l.name==null?null:l.name)&&y.getAttribute("property")===(l.property==null?null:l.property)&&y.getAttribute("http-equiv")===(l.httpEquiv==null?null:l.httpEquiv)&&y.getAttribute("charset")===(l.charSet==null?null:l.charSet)){_.splice(D,1);break e}}y=p.createElement(h),en(y,h,l),p.head.appendChild(y);break;default:throw Error(i(468,h))}y[Le]=s,Ye(y),h=y}s.stateNode=h}else Dg(p,s.type,s.stateNode);else s.stateNode=Og(p,h,s.memoizedProps);else y!==h?(y===null?l.stateNode!==null&&(l=l.stateNode,l.parentNode.removeChild(l)):y.count--,h===null?Dg(p,s.type,s.stateNode):Og(p,h,s.memoizedProps)):h===null&&s.stateNode!==null&&o1(s,s.memoizedProps,l.memoizedProps)}break;case 27:vn(r,s),bn(s),h&512&&(ke||l===null||gi(l,l.return)),l!==null&&h&4&&o1(s,s.memoizedProps,l.memoizedProps);break;case 5:if(vn(r,s),bn(s),h&512&&(ke||l===null||gi(l,l.return)),s.flags&32){p=s.stateNode;try{Yr(p,"")}catch(Ct){ue(s,s.return,Ct)}}h&4&&s.stateNode!=null&&(p=s.memoizedProps,o1(s,p,l!==null?l.memoizedProps:p)),h&1024&&(u1=!0);break;case 6:if(vn(r,s),bn(s),h&4){if(s.stateNode===null)throw Error(i(162));h=s.memoizedProps,l=s.stateNode;try{l.nodeValue=h}catch(Ct){ue(s,s.return,Ct)}}break;case 3:if(tu=null,p=ei,ei=Qc(r.containerInfo),vn(r,s),ei=p,bn(s),h&4&&l!==null&&l.memoizedState.isDehydrated)try{_a(r.containerInfo)}catch(Ct){ue(s,s.return,Ct)}u1&&(u1=!1,D4(s));break;case 4:h=ei,ei=Qc(s.stateNode.containerInfo),vn(r,s),bn(s),ei=h;break;case 12:vn(r,s),bn(s);break;case 31:vn(r,s),bn(s),h&4&&(h=s.updateQueue,h!==null&&(s.updateQueue=null,Uc(s,h)));break;case 13:vn(r,s),bn(s),s.child.flags&8192&&s.memoizedState!==null!=(l!==null&&l.memoizedState!==null)&&(zc=Be()),h&4&&(h=s.updateQueue,h!==null&&(s.updateQueue=null,Uc(s,h)));break;case 22:p=s.memoizedState!==null;var X=l!==null&&l.memoizedState!==null,nt=ji,lt=ke;if(ji=nt||p,ke=lt||X,vn(r,s),ke=lt,ji=nt,bn(s),h&8192)t:for(r=s.stateNode,r._visibility=p?r._visibility&-2:r._visibility|1,p&&(l===null||X||ji||ke||lr(s)),l=null,r=s;;){if(r.tag===5||r.tag===26){if(l===null){X=l=r;try{if(y=X.stateNode,p)_=y.style,typeof _.setProperty=="function"?_.setProperty("display","none","important"):_.display="none";else{D=X.stateNode;var ht=X.memoizedProps.style,it=ht!=null&&ht.hasOwnProperty("display")?ht.display:null;D.style.display=it==null||typeof it=="boolean"?"":(""+it).trim()}}catch(Ct){ue(X,X.return,Ct)}}}else if(r.tag===6){if(l===null){X=r;try{X.stateNode.nodeValue=p?"":X.memoizedProps}catch(Ct){ue(X,X.return,Ct)}}}else if(r.tag===18){if(l===null){X=r;try{var at=X.stateNode;p?Sg(at,!0):Sg(X.stateNode,!1)}catch(Ct){ue(X,X.return,Ct)}}}else if((r.tag!==22&&r.tag!==23||r.memoizedState===null||r===s)&&r.child!==null){r.child.return=r,r=r.child;continue}if(r===s)break t;for(;r.sibling===null;){if(r.return===null||r.return===s)break t;l===r&&(l=null),r=r.return}l===r&&(l=null),r.sibling.return=r.return,r=r.sibling}h&4&&(h=s.updateQueue,h!==null&&(l=h.retryQueue,l!==null&&(h.retryQueue=null,Uc(s,l))));break;case 19:vn(r,s),bn(s),h&4&&(h=s.updateQueue,h!==null&&(s.updateQueue=null,Uc(s,h)));break;case 30:break;case 21:break;default:vn(r,s),bn(s)}}function bn(s){var r=s.flags;if(r&2){try{for(var l,h=s.return;h!==null;){if(A4(h)){l=h;break}h=h.return}if(l==null)throw Error(i(160));switch(l.tag){case 27:var p=l.stateNode,y=l1(s);Fc(s,y,p);break;case 5:var _=l.stateNode;l.flags&32&&(Yr(_,""),l.flags&=-33);var D=l1(s);Fc(s,D,_);break;case 3:case 4:var X=l.stateNode.containerInfo,nt=l1(s);c1(s,nt,X);break;default:throw Error(i(161))}}catch(lt){ue(s,s.return,lt)}s.flags&=-3}r&4096&&(s.flags&=-4097)}function D4(s){if(s.subtreeFlags&1024)for(s=s.child;s!==null;){var r=s;D4(r),r.tag===5&&r.flags&1024&&r.stateNode.reset(),s=s.sibling}}function qi(s,r){if(r.subtreeFlags&8772)for(r=r.child;r!==null;)M4(s,r.alternate,r),r=r.sibling}function lr(s){for(s=s.child;s!==null;){var r=s;switch(r.tag){case 0:case 11:case 14:case 15:bs(4,r,r.return),lr(r);break;case 1:gi(r,r.return);var l=r.stateNode;typeof l.componentWillUnmount=="function"&&S4(r,r.return,l),lr(r);break;case 27:qo(r.stateNode);case 26:case 5:gi(r,r.return),lr(r);break;case 22:r.memoizedState===null&&lr(r);break;case 30:lr(r);break;default:lr(r)}s=s.sibling}}function Xi(s,r,l){for(l=l&&(r.subtreeFlags&8772)!==0,r=r.child;r!==null;){var h=r.alternate,p=s,y=r,_=y.flags;switch(y.tag){case 0:case 11:case 15:Xi(p,y,l),Bo(4,y);break;case 1:if(Xi(p,y,l),h=y,p=h.stateNode,typeof p.componentDidMount=="function")try{p.componentDidMount()}catch(nt){ue(h,h.return,nt)}if(h=y,p=h.updateQueue,p!==null){var D=h.stateNode;try{var X=p.shared.hiddenCallbacks;if(X!==null)for(p.shared.hiddenCallbacks=null,p=0;ppe&&(_=pe,pe=Mt,Mt=_);var Z=F3(D,Mt),$=F3(D,pe);if(Z&&$&&(at.rangeCount!==1||at.anchorNode!==Z.node||at.anchorOffset!==Z.offset||at.focusNode!==$.node||at.focusOffset!==$.offset)){var et=ht.createRange();et.setStart(Z.node,Z.offset),at.removeAllRanges(),Mt>pe?(at.addRange(et),at.extend($.node,$.offset)):(et.setEnd($.node,$.offset),at.addRange(et))}}}}for(ht=[],at=D;at=at.parentNode;)at.nodeType===1&&ht.push({element:at,left:at.scrollLeft,top:at.scrollTop});for(typeof D.focus=="function"&&D.focus(),D=0;Dl?32:l,A.T=null,l=y1,y1=null;var y=Ss,_=$i;if(je=0,ya=Ss=null,$i=0,(se&6)!==0)throw Error(i(331));var D=se;if(se|=4,U4(y.current),B4(y,y.current,_,l),se=D,Go(0,!1),Ze&&typeof Ze.onPostCommitFiberRoot=="function")try{Ze.onPostCommitFiberRoot(qs,y)}catch{}return!0}finally{B.p=p,A.T=h,ng(s,r)}}function sg(s,r,l){r=Un(l,r),r=Zf(s.stateNode,r,2),s=gs(s,r,2),s!==null&&(as(s,2),yi(s))}function ue(s,r,l){if(s.tag===3)sg(s,s,l);else for(;r!==null;){if(r.tag===3){sg(r,s,l);break}else if(r.tag===1){var h=r.stateNode;if(typeof r.type.getDerivedStateFromError=="function"||typeof h.componentDidCatch=="function"&&(Cs===null||!Cs.has(h))){s=Un(l,s),l=r4(2),h=gs(r,l,2),h!==null&&(a4(l,h,r,s),as(h,2),yi(h));break}}r=r.return}}function T1(s,r,l){var h=s.pingCache;if(h===null){h=s.pingCache=new sT;var p=new Set;h.set(r,p)}else p=h.get(r),p===void 0&&(p=new Set,h.set(r,p));p.has(l)||(d1=!0,p.add(l),s=cT.bind(null,s,r,l),r.then(s,s))}function cT(s,r,l){var h=s.pingCache;h!==null&&h.delete(r),s.pingedLanes|=s.suspendedLanes&l,s.warmLanes&=~l,ve===s&&(qt&l)===l&&(we===4||we===3&&(qt&62914560)===qt&&300>Be()-zc?(se&2)===0&&va(s,0):m1|=l,ga===qt&&(ga=0)),yi(s)}function rg(s,r){r===0&&(r=Ri()),s=Ks(s,r),s!==null&&(as(s,r),yi(s))}function uT(s){var r=s.memoizedState,l=0;r!==null&&(l=r.retryLane),rg(s,l)}function hT(s,r){var l=0;switch(s.tag){case 31:case 13:var h=s.stateNode,p=s.memoizedState;p!==null&&(l=p.retryLane);break;case 19:h=s.stateNode;break;case 22:h=s.stateNode._retryCache;break;default:throw Error(i(314))}h!==null&&h.delete(r),rg(s,l)}function fT(s,r){return Dn(s,r)}var Yc=null,xa=null,C1=!1,$c=!1,S1=!1,As=0;function yi(s){s!==xa&&s.next===null&&(xa===null?Yc=xa=s:xa=xa.next=s),$c=!0,C1||(C1=!0,mT())}function Go(s,r){if(!S1&&$c){S1=!0;do for(var l=!1,h=Yc;h!==null;){if(s!==0){var p=h.pendingLanes;if(p===0)var y=0;else{var _=h.suspendedLanes,D=h.pingedLanes;y=(1<<31-Q(42|s)+1)-1,y&=p&~(_&~D),y=y&201326741?y&201326741|1:y?y|2:0}y!==0&&(l=!0,cg(h,y))}else y=qt,y=Xe(h,h===ve?y:0,h.cancelPendingCommit!==null||h.timeoutHandle!==-1),(y&3)===0||pn(h,y)||(l=!0,cg(h,y));h=h.next}while(l);S1=!1}}function dT(){ag()}function ag(){$c=C1=!1;var s=0;As!==0&&_T()&&(s=As);for(var r=Be(),l=null,h=Yc;h!==null;){var p=h.next,y=og(h,r);y===0?(h.next=null,l===null?Yc=p:l.next=p,p===null&&(xa=l)):(l=h,(s!==0||(y&3)!==0)&&($c=!0)),h=p}je!==0&&je!==5||Go(s),As!==0&&(As=0)}function og(s,r){for(var l=s.suspendedLanes,h=s.pingedLanes,p=s.expirationTimes,y=s.pendingLanes&-62914561;0D)break;var lt=X.transferSize,ht=X.initiatorType;lt&&yg(ht)&&(X=X.responseEnd,_+=lt*(X"u"?null:document;function Rg(s,r,l){var h=Ta;if(h&&typeof r=="string"&&r){var p=Pn(r);p='link[rel="'+s+'"][href="'+p+'"]',typeof l=="string"&&(p+='[crossorigin="'+l+'"]'),Mg.has(p)||(Mg.add(p),s={rel:s,crossOrigin:l,href:r},h.querySelector(p)===null&&(r=h.createElement("link"),en(r,"link",s),Ye(r),h.head.appendChild(r)))}}function IT(s){Wi.D(s),Rg("dns-prefetch",s,null)}function DT(s,r){Wi.C(s,r),Rg("preconnect",s,r)}function BT(s,r,l){Wi.L(s,r,l);var h=Ta;if(h&&s&&r){var p='link[rel="preload"][as="'+Pn(r)+'"]';r==="image"&&l&&l.imageSrcSet?(p+='[imagesrcset="'+Pn(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(p+='[imagesizes="'+Pn(l.imageSizes)+'"]')):p+='[href="'+Pn(s)+'"]';var y=p;switch(r){case"style":y=Ca(s);break;case"script":y=Sa(s)}Hn.has(y)||(s=g({rel:"preload",href:r==="image"&&l&&l.imageSrcSet?void 0:s,as:r},l),Hn.set(y,s),h.querySelector(p)!==null||r==="style"&&h.querySelector(Xo(y))||r==="script"&&h.querySelector(Yo(y))||(r=h.createElement("link"),en(r,"link",s),Ye(r),h.head.appendChild(r)))}}function PT(s,r){Wi.m(s,r);var l=Ta;if(l&&s){var h=r&&typeof r.as=="string"?r.as:"script",p='link[rel="modulepreload"][as="'+Pn(h)+'"][href="'+Pn(s)+'"]',y=p;switch(h){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":y=Sa(s)}if(!Hn.has(y)&&(s=g({rel:"modulepreload",href:s},r),Hn.set(y,s),l.querySelector(p)===null)){switch(h){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(Yo(y)))return}h=l.createElement("link"),en(h,"link",s),Ye(h),l.head.appendChild(h)}}}function FT(s,r,l){Wi.S(s,r,l);var h=Ta;if(h&&s){var p=Hr(h).hoistableStyles,y=Ca(s);r=r||"default";var _=p.get(y);if(!_){var D={loading:0,preload:null};if(_=h.querySelector(Xo(y)))D.loading=5;else{s=g({rel:"stylesheet",href:s,"data-precedence":r},l),(l=Hn.get(y))&&k1(s,l);var X=_=h.createElement("link");Ye(X),en(X,"link",s),X._p=new Promise(function(nt,lt){X.onload=nt,X.onerror=lt}),X.addEventListener("load",function(){D.loading|=1}),X.addEventListener("error",function(){D.loading|=2}),D.loading|=4,Jc(_,r,h)}_={type:"stylesheet",instance:_,count:1,state:D},p.set(y,_)}}}function UT(s,r){Wi.X(s,r);var l=Ta;if(l&&s){var h=Hr(l).hoistableScripts,p=Sa(s),y=h.get(p);y||(y=l.querySelector(Yo(p)),y||(s=g({src:s,async:!0},r),(r=Hn.get(p))&&z1(s,r),y=l.createElement("script"),Ye(y),en(y,"link",s),l.head.appendChild(y)),y={type:"script",instance:y,count:1,state:null},h.set(p,y))}}function kT(s,r){Wi.M(s,r);var l=Ta;if(l&&s){var h=Hr(l).hoistableScripts,p=Sa(s),y=h.get(p);y||(y=l.querySelector(Yo(p)),y||(s=g({src:s,async:!0,type:"module"},r),(r=Hn.get(p))&&z1(s,r),y=l.createElement("script"),Ye(y),en(y,"link",s),l.head.appendChild(y)),y={type:"script",instance:y,count:1,state:null},h.set(p,y))}}function Lg(s,r,l,h){var p=(p=ct.current)?Qc(p):null;if(!p)throw Error(i(446));switch(s){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(r=Ca(l.href),l=Hr(p).hoistableStyles,h=l.get(r),h||(h={type:"style",instance:null,count:0,state:null},l.set(r,h)),h):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){s=Ca(l.href);var y=Hr(p).hoistableStyles,_=y.get(s);if(_||(p=p.ownerDocument||p,_={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},y.set(s,_),(y=p.querySelector(Xo(s)))&&!y._p&&(_.instance=y,_.state.loading=5),Hn.has(s)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Hn.set(s,l),y||zT(p,s,l,_.state))),r&&h===null)throw Error(i(528,""));return _}if(r&&h!==null)throw Error(i(529,""));return null;case"script":return r=l.async,l=l.src,typeof l=="string"&&r&&typeof r!="function"&&typeof r!="symbol"?(r=Sa(l),l=Hr(p).hoistableScripts,h=l.get(r),h||(h={type:"script",instance:null,count:0,state:null},l.set(r,h)),h):{type:"void",instance:null,count:0,state:null};default:throw Error(i(444,s))}}function Ca(s){return'href="'+Pn(s)+'"'}function Xo(s){return'link[rel="stylesheet"]['+s+"]"}function Ng(s){return g({},s,{"data-precedence":s.precedence,precedence:null})}function zT(s,r,l,h){s.querySelector('link[rel="preload"][as="style"]['+r+"]")?h.loading=1:(r=s.createElement("link"),h.preload=r,r.addEventListener("load",function(){return h.loading|=1}),r.addEventListener("error",function(){return h.loading|=2}),en(r,"link",l),Ye(r),s.head.appendChild(r))}function Sa(s){return'[src="'+Pn(s)+'"]'}function Yo(s){return"script[async]"+s}function Og(s,r,l){if(r.count++,r.instance===null)switch(r.type){case"style":var h=s.querySelector('style[data-href~="'+Pn(l.href)+'"]');if(h)return r.instance=h,Ye(h),h;var p=g({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return h=(s.ownerDocument||s).createElement("style"),Ye(h),en(h,"style",p),Jc(h,l.precedence,s),r.instance=h;case"stylesheet":p=Ca(l.href);var y=s.querySelector(Xo(p));if(y)return r.state.loading|=4,r.instance=y,Ye(y),y;h=Ng(l),(p=Hn.get(p))&&k1(h,p),y=(s.ownerDocument||s).createElement("link"),Ye(y);var _=y;return _._p=new Promise(function(D,X){_.onload=D,_.onerror=X}),en(y,"link",h),r.state.loading|=4,Jc(y,l.precedence,s),r.instance=y;case"script":return y=Sa(l.src),(p=s.querySelector(Yo(y)))?(r.instance=p,Ye(p),p):(h=l,(p=Hn.get(y))&&(h=g({},l),z1(h,p)),s=s.ownerDocument||s,p=s.createElement("script"),Ye(p),en(p,"link",h),s.head.appendChild(p),r.instance=p);case"void":return null;default:throw Error(i(443,r.type))}else r.type==="stylesheet"&&(r.state.loading&4)===0&&(h=r.instance,r.state.loading|=4,Jc(h,l.precedence,s));return r.instance}function Jc(s,r,l){for(var h=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),p=h.length?h[h.length-1]:null,y=p,_=0;_ title"):null)}function VT(s,r,l){if(l===1||r.itemProp!=null)return!1;switch(s){case"meta":case"title":return!0;case"style":if(typeof r.precedence!="string"||typeof r.href!="string"||r.href==="")break;return!0;case"link":if(typeof r.rel!="string"||typeof r.href!="string"||r.href===""||r.onLoad||r.onError)break;return r.rel==="stylesheet"?(s=r.disabled,typeof r.precedence=="string"&&s==null):!0;case"script":if(r.async&&typeof r.async!="function"&&typeof r.async!="symbol"&&!r.onLoad&&!r.onError&&r.src&&typeof r.src=="string")return!0}return!1}function Bg(s){return!(s.type==="stylesheet"&&(s.state.loading&3)===0)}function GT(s,r,l,h){if(l.type==="stylesheet"&&(typeof h.media!="string"||matchMedia(h.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var p=Ca(h.href),y=r.querySelector(Xo(p));if(y){r=y._p,r!==null&&typeof r=="object"&&typeof r.then=="function"&&(s.count++,s=eu.bind(s),r.then(s,s)),l.state.loading|=4,l.instance=y,Ye(y);return}y=r.ownerDocument||r,h=Ng(h),(p=Hn.get(p))&&k1(h,p),y=y.createElement("link"),Ye(y);var _=y;_._p=new Promise(function(D,X){_.onload=D,_.onerror=X}),en(y,"link",h),l.instance=y}s.stylesheets===null&&(s.stylesheets=new Map),s.stylesheets.set(l,r),(r=l.state.preload)&&(l.state.loading&3)===0&&(s.count++,l=eu.bind(s),r.addEventListener("load",l),r.addEventListener("error",l))}}var V1=0;function jT(s,r){return s.stylesheets&&s.count===0&&iu(s,s.stylesheets),0V1?50:800)+r);return s.unsuspend=l,function(){s.unsuspend=null,clearTimeout(h),clearTimeout(p)}}:null}function eu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)iu(this,this.stylesheets);else if(this.unsuspend){var s=this.unsuspend;this.unsuspend=null,s()}}}var nu=null;function iu(s,r){s.stylesheets=null,s.unsuspend!==null&&(s.count++,nu=new Map,r.forEach(HT,s),nu=null,eu.call(s))}function HT(s,r){if(!(r.state.loading&4)){var l=nu.get(s);if(l)var h=l.get(null);else{l=new Map,nu.set(s,l);for(var p=s.querySelectorAll("link[data-precedence],style[data-precedence]"),y=0;y"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(t){console.error(t)}}return n(),Z1.exports=aC(),Z1.exports}var lC=oC();const ts={BASE_URL:"./",DEV:!1,MODE:"production",PROD:!0,SSR:!1};function X8(n){return"init"in n}function k0(n){return typeof n.write=="function"}function cC(n){return!!n.onMount}function ry(n){return"v"in n||"e"in n}function Ku(n){if("e"in n)throw n.e;if((ts?"production":void 0)!=="production"&&!("v"in n))throw new Error("[Bug] atom state is not initialized");return n.v}function Qu(n){return typeof n?.then=="function"}function uC(n){if(!(n instanceof Error))return!1;const t=n.name,e=n.message.toLowerCase();return(t==="RangeError"||t==="InternalError")&&(e.includes("call stack")||e.includes("too much recursion")||e.includes("stack overflow"))}function Y8(n,t,e){if(!e.p.has(n)){e.p.add(n);const i=()=>e.p.delete(n);t.then(i,i)}}function $8(n,t,e){const i=e.get(n),a=i?.t,o=t.p;if(!a?.size)return o;if(!o.size)return a;const c=new Set(a);for(const u of o)c.add(u);return c}function hC(n){return!!n.INTERNAL_onInit}const fC=(n,t,e,...i)=>e.read(...i),dC=(n,t,e,...i)=>e.write(...i),mC=(n,t,e)=>e.INTERNAL_onInit(t),pC=(n,t,e,i)=>{var a;return(a=e.onMount)==null?void 0:a.call(e,i)},gC=(n,t,e)=>{var i;const a=n[0];let o=a.get(e);if(!o){const c=n[6],u=n[9];o={d:new Map,p:new Set,n:0},a.set(e,o),(i=c.i)==null||i.call(c,e),hC(e)&&u(n,t,e)}return o},yC=(n,t)=>{var e;const i=n[1],a=n[3],o=n[4],c=n[5],u=n[6],f=n[13];if(!u.f&&!a.size&&!o.size&&!c.size)return;const d=[],m=g=>{try{g()}catch(v){d.push(v)}};do{u.f&&m(u.f);const g=new Set;for(const v of a){const x=(e=i.get(v))==null?void 0:e.l;if(x)for(const b of x)g.add(b)}a.clear();for(const v of c)g.add(v);c.clear();for(const v of o)g.add(v);o.clear();for(const v of g)m(v);a.size&&f(n,t)}while(a.size||c.size||o.size);if(d.length)throw new AggregateError(d)},vC=(n,t)=>{const e=n[1],i=n[2],a=n[3],o=n[11],c=n[14],u=n[17];if(!a.size)return;const f=[],d=[],m=new WeakSet,g=new WeakSet,v=[],x=[];for(const b of a)v.push(b),x.push(o(n,t,b));for(;v.length;){const b=v.length-1,C=v[b],T=x[b];if(g.has(C)){v.pop(),x.pop();continue}if(m.has(C)){if(i.get(C)===T.n)f.push(C),d.push(T);else if((ts?"production":void 0)!=="production"&&i.has(C))throw new Error("[Bug] invalidated atom exists");g.add(C),v.pop(),x.pop();continue}m.add(C);for(const S of $8(C,T,e))m.has(S)||(v.push(S),x.push(o(n,t,S)))}for(let b=f.length-1;b>=0;--b){const C=f[b],T=d[b];let S=!1;for(const L of T.d.keys())if(L!==C&&a.has(L)){S=!0;break}S&&(i.set(C,T.n),c(n,t,C),u(n,t,C)),i.delete(C)}},z0=new WeakSet,bC=(n,t,e)=>{var i,a;const o=n[1],c=n[2],u=n[3],f=n[6],d=n[7],m=n[11],g=n[12],v=n[13],x=n[14],b=n[16],C=n[17],T=n[20],S=n[26],L=n[28],R=m(n,t,e),P=L[0];if(ry(R)){if(o.has(e)&&c.get(e)!==R.n||R.m===P)return R.m=P,R;let K=!1;for(const[A,B]of R.d)if(x(n,t,A).n!==B){K=!0;break}if(!K)return R.m=P,R}let w=!0;const F=new Set(R.d.keys()),E=()=>{for(const K of F)R.d.delete(K)},O=()=>{if(o.has(e)){const K=!u.size;C(n,t,e),K&&(v(n,t),g(n,t))}},I=K=>{var A;if(K===e){const N=m(n,t,K);if(!ry(N))if(X8(K))T(n,t,K,K.init);else throw new Error("no atom init");return Ku(N)}const B=x(n,t,K);try{return Ku(B)}finally{F.delete(K),R.d.set(K,B.n),Qu(R.v)&&Y8(e,R.v,B),o.has(e)&&((A=o.get(K))==null||A.t.add(e)),w||O()}};let k,U;const G={get signal(){return k||(k=new AbortController),k.signal},get setSelf(){return(ts?"production":void 0)!=="production"&&console.warn("[DEPRECATED] setSelf is deprecated and will be removed in v3."),(ts?"production":void 0)!=="production"&&!k0(e)&&console.warn("setSelf function cannot be used with read-only atom"),!U&&k0(e)&&(U=(...K)=>{if((ts?"production":void 0)!=="production"&&w&&console.warn("setSelf function cannot be called in sync"),!w)try{return b(n,t,e,K)}finally{v(n,t),g(n,t)}}),U}},J=R.n,tt=c.get(e)===J;try{(ts?"production":void 0)!=="production"&&z0.delete(t);const K=d(n,t,e,I,G);if((ts?"production":void 0)!=="production"&&z0.has(t)&&console.warn("Detected store mutation during atom read. This is not supported."),T(n,t,e,K),Qu(K)){S(n,t,K,()=>k?.abort());const A=()=>{E(),O()};K.then(A,A)}else E();return(i=f.r)==null||i.call(f,e),R.m=P,R}catch(K){if(uC(K))throw K;return delete R.v,R.e=K,++R.n,R.m=P,R}finally{w=!1,R.n!==J&&tt&&(c.set(e,R.n),u.add(e),(a=f.c)==null||a.call(f,e))}},xC=(n,t,e)=>{const i=n[1],a=n[2],o=n[11],c=[e];for(;c.length;){const u=c.pop(),f=o(n,t,u);for(const d of $8(u,f,i)){const m=o(n,t,d);a.get(d)!==m.n&&(a.set(d,m.n),c.push(d))}}},TC=(n,t,e,i)=>{const a=n[3],o=n[6],c=n[8],u=n[11],f=n[12],d=n[13],m=n[14],g=n[15],v=n[16],x=n[17],b=n[20],C=n[28];let T=!0;const S=R=>Ku(m(n,t,R)),L=(R,...P)=>{var w;const F=u(n,t,R);try{if(R===e){if(!X8(R))throw new Error("atom not writable");(ts?"production":void 0)!=="production"&&z0.add(t);const E=F.n,O=P[0];b(n,t,R,O),x(n,t,R),E!==F.n&&(++C[0],a.add(R),g(n,t,R),(w=o.c)==null||w.call(o,R));return}else return v(n,t,R,P)}finally{T||(d(n,t),f(n,t))}};try{return c(n,t,e,S,L,...i)}finally{T=!1}},CC=(n,t,e)=>{var i;const a=n[1],o=n[3],c=n[6],u=n[11],f=n[15],d=n[18],m=n[19],g=u(n,t,e),v=a.get(e);if(v&&g.d.size>0){for(const[x,b]of g.d)if(!v.d.has(x)){const C=u(n,t,x);d(n,t,x).t.add(e),v.d.add(x),b!==C.n&&(o.add(x),f(n,t,x),(i=c.c)==null||i.call(c,x))}for(const x of v.d)if(!g.d.has(x)){v.d.delete(x);const b=m(n,t,x);b?.t.delete(e)}}},SC=(n,t,e)=>{var i;const a=n[1],o=n[4],c=n[6],u=n[10],f=n[11],d=n[12],m=n[13],g=n[14],v=n[16],x=n[18],b=f(n,t,e);let C=a.get(e);if(!C){g(n,t,e);for(const T of b.d.keys())x(n,t,T).t.add(e);if(C={l:new Set,d:new Set(b.d.keys()),t:new Set},a.set(e,C),k0(e)&&cC(e)){const T=()=>{let S=!0;const L=(...R)=>{try{return v(n,t,e,R)}finally{S||(m(n,t),d(n,t))}};try{const R=u(n,t,e,L);R&&(C.u=()=>{S=!0;try{R()}finally{S=!1}})}finally{S=!1}};o.add(T)}(i=c.m)==null||i.call(c,e)}return C},_C=(n,t,e)=>{var i,a;const o=n[1],c=n[5],u=n[6],f=n[11],d=n[19],m=f(n,t,e);let g=o.get(e);if(!g||g.l.size)return g;let v=!1;for(const x of g.t)if((i=o.get(x))!=null&&i.d.has(e)){v=!0;break}if(!v){g.u&&c.add(g.u),g=void 0,o.delete(e);for(const x of m.d.keys()){const b=d(n,t,x);b?.t.delete(e)}(a=u.u)==null||a.call(u,e);return}return g},AC=(n,t,e,i)=>{const a=n[11],o=n[27],c=a(n,t,e),u="v"in c,f=c.v;if(Qu(i))for(const d of c.d.keys())Y8(e,i,a(n,t,d));c.v=i,delete c.e,(!u||!Object.is(f,c.v))&&(++c.n,Qu(f)&&o(n,t,f))},EC=(n,t,e)=>{const i=n[14];return Ku(i(n,t,e))},wC=(n,t,e,...i)=>{const a=n[3],o=n[12],c=n[13],u=n[16],f=a.size;try{return u(n,t,e,i)}finally{a.size!==f&&(c(n,t),o(n,t))}},MC=(n,t,e,i)=>{const a=n[12],o=n[13],c=n[18],u=n[19],d=c(n,t,e).l;return d.add(i),o(n,t),a(n,t),()=>{d.delete(i),u(n,t,e),o(n,t),a(n,t)}},RC=(n,t,e,i)=>{const a=n[25];let o=a.get(e);if(!o){o=new Set,a.set(e,o);const c=()=>a.delete(e);e.then(c,c)}o.add(i)},LC=(n,t,e)=>{const a=n[25].get(e);a?.forEach(o=>o())},W8=new WeakMap;function NC(n){const t=W8.get(n);if((ts?"production":void 0)!=="production"&&!t)throw new Error("Store must be created by buildStore to read its building blocks");const e=t[24];return e?e(t,n):t}function OC(...n){const t={get(c){return i(e,t,c)},set(c,...u){return a(e,t,c,...u)},sub(c,u){return o(e,t,c,u)}},e=[new WeakMap,new WeakMap,new WeakMap,new Set,new Set,new Set,{},fC,dC,mC,pC,gC,yC,vC,bC,xC,TC,CC,SC,_C,AC,EC,wC,MC,void 0,new WeakMap,RC,LC,[0]].map((c,u)=>n[u]||c);W8.set(t,Object.freeze(e));const i=e[21],a=e[22],o=e[23];return t}const Z8={};let IC=0;function zt(n,t){const e=`atom${++IC}`,i={toString(){return(Z8?"production":void 0)!=="production"&&this.debugLabel?e+":"+this.debugLabel:e}};return typeof n=="function"?i.read=n:(i.init=n,i.read=DC,i.write=BC),t&&(i.write=t),i}function DC(n){return n(this)}function BC(n,t,e){return t(this,typeof e=="function"?e(n(this)):e)}function Hm(){return OC()}let tl;function PC(){return tl||(tl=Hm(),(Z8?"production":void 0)!=="production"&&(globalThis.__JOTAI_DEFAULT_STORE__||(globalThis.__JOTAI_DEFAULT_STORE__=tl),globalThis.__JOTAI_DEFAULT_STORE__!==tl&&console.warn("Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044"))),tl}const FC={},V0=q.createContext(void 0);function K8(n){return q.useContext(V0)||PC()}function UC({children:n,store:t}){const e=q.useRef(null);return t?q.createElement(V0.Provider,{value:t},n):(e.current===null&&(e.current=Hm()),q.createElement(V0.Provider,{value:e.current},n))}const G0=n=>typeof n?.then=="function",j0=n=>{n.status||(n.status="pending",n.then(t=>{n.status="fulfilled",n.value=t},t=>{n.status="rejected",n.reason=t}))},kC=Gl.use||(n=>{if(n.status==="pending")throw n;if(n.status==="fulfilled")return n.value;throw n.status==="rejected"?n.reason:(j0(n),n)}),td=new WeakMap,ay=(n,t,e)=>{const i=NC(n),a=i[26];let o=td.get(t);return o||(o=new Promise((c,u)=>{let f=t;const d=v=>x=>{f===v&&c(x)},m=v=>x=>{f===v&&u(x)},g=()=>{try{const v=e();G0(v)?(td.set(v,o),f=v,v.then(d(v),m(v)),a(i,n,v,g)):c(v)}catch(v){u(v)}};t.then(d(t),m(t)),a(i,n,t,g)}),td.set(t,o)),o};function Tt(n,t){const{delay:e,unstable_promiseStatus:i=!Gl.use}={},a=K8(),[[o,c,u],f]=q.useReducer(m=>{const g=a.get(n);return Object.is(m[0],g)&&m[1]===a&&m[2]===n?m:[g,a,n]},void 0,()=>[a.get(n),a,n]);let d=o;if((c!==a||u!==n)&&(f(),d=a.get(n)),q.useEffect(()=>{const m=a.sub(n,()=>{if(i)try{const g=a.get(n);G0(g)&&j0(ay(a,g,()=>a.get(n)))}catch{}if(typeof e=="number"){console.warn(`[DEPRECATED] delay option is deprecated and will be removed in v3. + +Migration guide: + +Create a custom hook like the following. + +function useAtomValueWithDelay( + atom: Atom, + options: { delay: number }, +): Value { + const { delay } = options + const store = useStore(options) + const [value, setValue] = useState(() => store.get(atom)) + useEffect(() => { + const unsub = store.sub(atom, () => { + setTimeout(() => setValue(store.get(atom)), delay) + }) + return unsub + }, [store, atom, delay]) + return value +} +`),setTimeout(f,e);return}f()});return f(),m},[a,n,e,i]),q.useDebugValue(d),G0(d)){const m=ay(a,d,()=>a.get(n));return i&&j0(m),kC(m)}return d}function Ju(n,t){const e=K8();return q.useCallback((...a)=>{if((FC?"production":void 0)!=="production"&&!("write"in n))throw new Error("not writable atom");return e.set(n,...a)},[e,n])}function qm(n,t){return[Tt(n),Ju(n)]}var ed={exports:{}},nd={};var oy;function zC(){if(oy)return nd;oy=1;var n=vh().__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;return nd.c=function(t){return n.H.useMemoCache(t)},nd}var ly;function VC(){return ly||(ly=1,ed.exports=zC()),ed.exports}var Re=VC(),id={exports:{}};var cy;function GC(){return cy||(cy=1,(function(n){(function(){var t={}.hasOwnProperty;function e(){for(var o="",c=0;c-1&&n.splice(e,1)}const Ei=(n,t,e)=>e>t?t:e{};const zs={},Q8=n=>/^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(n),J8=n=>typeof n=="object"&&n!==null,t7=n=>/^0[^.\s]+$/u.test(n);function e7(n){let t;return()=>(t===void 0&&(t=n()),t)}const Zn=n=>n,Hl=(...n)=>n.reduce((t,e)=>i=>e(t(i))),Xa=(n,t,e)=>{const i=t-n;return i?(e-n)/i:1};class Ym{constructor(){this.subscriptions=[]}add(t){return Xm(this.subscriptions,t),()=>qa(this.subscriptions,t)}notify(t,e,i){const a=this.subscriptions.length;if(a)if(a===1)this.subscriptions[0](t,e,i);else for(let o=0;on*1e3,Xn=n=>n/1e3,n7=(n,t)=>t?n*(1e3/t):0,qC=(n,t,e)=>{const i=t-n;return((e-n)%i+i)%i+n},i7=(n,t,e)=>(((1-3*e+3*t)*n+(3*e-6*t))*n+3*t)*n,XC=1e-7,YC=12;function $C(n,t,e,i,a){let o,c,u=0;do c=t+(e-t)/2,o=i7(c,i,a)-n,o>0?e=c:t=c;while(Math.abs(o)>XC&&++u$C(o,0,1,n,e);return o=>o===0||o===1?o:i7(a(o),t,i)}const s7=n=>t=>t<=.5?n(2*t)/2:(2-n(2*(1-t)))/2,$m=n=>t=>1-n(1-t),r7=ql(.33,1.53,.69,.99),Wm=$m(r7),a7=s7(Wm),o7=n=>n>=1?1:(n*=2)<1?.5*Wm(n):.5*(2-Math.pow(2,-10*(n-1))),Zm=n=>1-Math.sin(Math.acos(n)),l7=$m(Zm),c7=s7(Zm),WC=ql(.42,0,1,1),ZC=ql(0,0,.58,1),u7=ql(.42,0,.58,1),h7=n=>Array.isArray(n)&&typeof n[0]!="number";function f7(n,t){return h7(n)?n[qC(0,n.length,t)]:n}const d7=n=>Array.isArray(n)&&typeof n[0]=="number",KC={linear:Zn,easeIn:WC,easeInOut:u7,easeOut:ZC,circIn:Zm,circInOut:c7,circOut:l7,backIn:Wm,backInOut:a7,backOut:r7,anticipate:o7},QC=n=>typeof n=="string",uy=n=>{if(d7(n)){xh(n.length===4);const[t,e,i,a]=n;return ql(t,e,i,a)}else if(QC(n))return KC[n];return n},hu=["setup","read","resolveKeyframes","preUpdate","update","preRender","render","postRender"];function JC(n){let t=new Set,e=new Set,i=!1,a=!1;const o=new WeakSet;let c={delta:0,timestamp:0,isProcessing:!1};function u(d){o.has(d)&&(f.schedule(d),n()),d(c)}const f={schedule:(d,m=!1,g=!1)=>{const x=g&&i?t:e;return m&&o.add(d),x.add(d),d},cancel:d=>{e.delete(d),o.delete(d)},process:d=>{if(c=d,i){a=!0;return}i=!0;const m=t;t=e,e=m,t.forEach(u),t.clear(),i=!1,a&&(a=!1,f.process(d))}};return f}const tS=40;function m7(n,t){let e=!1,i=!0;const a={delta:0,timestamp:0,isProcessing:!1},o=()=>e=!0,c=hu.reduce((R,P)=>(R[P]=JC(o),R),{}),{setup:u,read:f,resolveKeyframes:d,preUpdate:m,update:g,preRender:v,render:x,postRender:b}=c,C=()=>{const R=zs.useManualTiming,P=R?a.timestamp:performance.now();e=!1,R||(a.delta=i?1e3/60:Math.max(Math.min(P-a.timestamp,tS),1)),a.timestamp=P,a.isProcessing=!0,u.process(a),f.process(a),d.process(a),m.process(a),g.process(a),v.process(a),x.process(a),b.process(a),a.isProcessing=!1,e&&t&&(i=!1,n(C))},T=()=>{e=!0,i=!0,a.isProcessing||n(C)};return{schedule:hu.reduce((R,P)=>{const w=c[P];return R[P]=(F,E=!1,O=!1)=>(e||T(),w.schedule(F,E,O)),R},{}),cancel:R=>{for(let P=0;P(Bu===void 0&&dn.set(nn.isProcessing||zs.useManualTiming?nn.timestamp:performance.now()),Bu),set:n=>{Bu=n,queueMicrotask(eS)}},p7=n=>t=>typeof t=="string"&&t.startsWith(n),g7=p7("--"),nS=p7("var(--"),Km=n=>nS(n)?iS.test(n.split("/*")[0].trim()):!1,iS=/var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu;function hy(n){return typeof n!="string"?!1:n.split("/*")[0].includes("var(--")}const Za={test:n=>typeof n=="number",parse:parseFloat,transform:n=>n},Ol={...Za,transform:n=>Ei(0,1,n)},fu={...Za,default:1},Tl=n=>Math.round(n*1e5)/1e5,Qm=/-?(?:\d+(?:\.\d+)?|\.\d+)/gu;function sS(n){return n==null}const rS=/^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu,Jm=(n,t)=>e=>!!(typeof e=="string"&&rS.test(e)&&e.startsWith(n)||t&&!sS(e)&&Object.prototype.hasOwnProperty.call(e,t)),y7=(n,t,e)=>i=>{if(typeof i!="string")return i;const[a,o,c,u]=i.match(Qm);return{[n]:parseFloat(a),[t]:parseFloat(o),[e]:parseFloat(c),alpha:u!==void 0?parseFloat(u):1}},aS=n=>Ei(0,255,n),rd={...Za,transform:n=>Math.round(aS(n))},_r={test:Jm("rgb","red"),parse:y7("red","green","blue"),transform:({red:n,green:t,blue:e,alpha:i=1})=>"rgba("+rd.transform(n)+", "+rd.transform(t)+", "+rd.transform(e)+", "+Tl(Ol.transform(i))+")"};function oS(n){let t="",e="",i="",a="";return n.length>5?(t=n.substring(1,3),e=n.substring(3,5),i=n.substring(5,7),a=n.substring(7,9)):(t=n.substring(1,2),e=n.substring(2,3),i=n.substring(3,4),a=n.substring(4,5),t+=t,e+=e,i+=i,a+=a),{red:parseInt(t,16),green:parseInt(e,16),blue:parseInt(i,16),alpha:a?parseInt(a,16)/255:1}}const H0={test:Jm("#"),parse:oS,transform:_r.transform},Xl=n=>({test:t=>typeof t=="string"&&t.endsWith(n)&&t.split(" ").length===1,parse:parseFloat,transform:t=>`${t}${n}`}),Qi=Xl("deg"),Si=Xl("%"),_t=Xl("px"),lS=Xl("vh"),cS=Xl("vw"),fy={...Si,parse:n=>Si.parse(n)/100,transform:n=>Si.transform(n*100)},Pa={test:Jm("hsl","hue"),parse:y7("hue","saturation","lightness"),transform:({hue:n,saturation:t,lightness:e,alpha:i=1})=>"hsla("+Math.round(n)+", "+Si.transform(Tl(t))+", "+Si.transform(Tl(e))+", "+Tl(Ol.transform(i))+")"},Ve={test:n=>_r.test(n)||H0.test(n)||Pa.test(n),parse:n=>_r.test(n)?_r.parse(n):Pa.test(n)?Pa.parse(n):H0.parse(n),transform:n=>typeof n=="string"?n:n.hasOwnProperty("red")?_r.transform(n):Pa.transform(n),getAnimatableNone:n=>{const t=Ve.parse(n);return t.alpha=0,Ve.transform(t)}},uS=/(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu;function hS(n){return isNaN(n)&&typeof n=="string"&&(n.match(Qm)?.length||0)+(n.match(uS)?.length||0)>0}const v7="number",b7="color",fS="var",dS="var(",dy="${}",mS=/var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;function Ya(n){const t=n.toString(),e=[],i={color:[],number:[],var:[]},a=[];let o=0;const u=t.replace(mS,f=>(Ve.test(f)?(i.color.push(o),a.push(b7),e.push(Ve.parse(f))):f.startsWith(dS)?(i.var.push(o),a.push(fS),e.push(f)):(i.number.push(o),a.push(v7),e.push(parseFloat(f))),++o,dy)).split(dy);return{values:e,split:u,indexes:i,types:a}}function pS(n){return Ya(n).values}function x7({split:n,types:t}){const e=n.length;return i=>{let a="";for(let o=0;otypeof n=="number"?0:Ve.test(n)?Ve.getAnimatableNone(n):n,vS=(n,t)=>typeof n=="number"?t?.trim().endsWith("/")?n:0:yS(n);function bS(n){const t=Ya(n);return x7(t)(t.values.map((i,a)=>vS(i,t.split[a])))}const ci={test:hS,parse:pS,createTransformer:gS,getAnimatableNone:bS};function ad(n,t,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?n+(t-n)*6*e:e<1/2?t:e<2/3?n+(t-n)*(2/3-e)*6:n}function xS({hue:n,saturation:t,lightness:e,alpha:i}){n/=360,t/=100,e/=100;let a=0,o=0,c=0;if(!t)a=o=c=e;else{const u=e<.5?e*(1+t):e+t-e*t,f=2*e-u;a=ad(f,u,n+1/3),o=ad(f,u,n),c=ad(f,u,n-1/3)}return{red:Math.round(a*255),green:Math.round(o*255),blue:Math.round(c*255),alpha:i}}function th(n,t){return e=>e>0?t:n}const ye=(n,t,e)=>n+(t-n)*e,od=(n,t,e)=>{const i=n*n,a=e*(t*t-i)+i;return a<0?0:Math.sqrt(a)},TS=[H0,_r,Pa],CS=n=>TS.find(t=>t.test(n));function my(n){const t=CS(n);if(!t)return!1;let e=t.parse(n);return t===Pa&&(e=xS(e)),e}const py=(n,t)=>{const e=my(n),i=my(t);if(!e||!i)return th(n,t);const a={...e};return o=>(a.red=od(e.red,i.red,o),a.green=od(e.green,i.green,o),a.blue=od(e.blue,i.blue,o),a.alpha=ye(e.alpha,i.alpha,o),_r.transform(a))},q0=new Set(["none","hidden"]);function SS(n,t){return q0.has(n)?e=>e<=0?n:t:e=>e>=1?t:n}function _S(n,t){return e=>ye(n,t,e)}function t2(n){return typeof n=="number"?_S:typeof n=="string"?Km(n)?th:Ve.test(n)?py:wS:Array.isArray(n)?T7:typeof n=="object"?Ve.test(n)?py:AS:th}function T7(n,t){const e=[...n],i=e.length,a=n.map((o,c)=>t2(o)(o,t[c]));return o=>{for(let c=0;c{for(const o in i)e[o]=i[o](a);return e}}function ES(n,t){const e=[],i={color:0,var:0,number:0};for(let a=0;a{const e=ci.createTransformer(t),i=Ya(n),a=Ya(t);return i.indexes.var.length===a.indexes.var.length&&i.indexes.color.length===a.indexes.color.length&&i.indexes.number.length>=a.indexes.number.length?q0.has(n)&&!a.values.length||q0.has(t)&&!i.values.length?SS(n,t):Hl(T7(ES(i,a),a.values),e):th(n,t)};function C7(n,t,e){return typeof n=="number"&&typeof t=="number"&&typeof e=="number"?ye(n,t,e):t2(n)(n,t)}const MS=n=>{const t=({timestamp:e})=>n(e);return{start:(e=!0)=>re.update(t,e),stop:()=>wi(t),now:()=>nn.isProcessing?nn.timestamp:dn.now()}},S7=(n,t,e=10)=>{let i="";const a=Math.max(Math.round(t/e),2);for(let o=0;o=eh?1/0:t}function _7(n,t=100,e){const i=e({...n,keyframes:[0,t]}),a=Math.min(e2(i),eh);return{type:"keyframes",ease:o=>i.next(a*o).value/t,duration:Xn(a)}}const Me={stiffness:100,damping:10,mass:1,velocity:0,duration:800,bounce:.3,visualDuration:.3,restSpeed:{granular:.01,default:2},restDelta:{granular:.005,default:.5},minDuration:.01,maxDuration:10,minDamping:.05,maxDamping:1};function X0(n,t){return n*Math.sqrt(1-t*t)}const RS=12;function LS(n,t,e){let i=e;for(let a=1;a{const m=d*c,g=m*n,v=m-e,x=X0(d,c),b=Math.exp(-g);return ld-v/x*b},o=d=>{const g=d*c*n,v=g*e+e,x=Math.pow(c,2)*Math.pow(d,2)*n,b=Math.exp(-g),C=X0(Math.pow(d,2),c);return(-a(d)+ld>0?-1:1)*((v-x)*b)/C}):(a=d=>{const m=Math.exp(-d*n),g=(d-e)*n+1;return-ld+m*g},o=d=>{const m=Math.exp(-d*n),g=(e-d)*(n*n);return m*g});const u=5/n,f=LS(a,o,u);if(n=Cn(n),isNaN(f))return{stiffness:Me.stiffness,damping:Me.damping,duration:n};{const d=Math.pow(f,2)*i;return{stiffness:d,damping:c*2*Math.sqrt(i*d),duration:n}}}const OS=["duration","bounce"],IS=["stiffness","damping","mass"];function gy(n,t){return t.some(e=>n[e]!==void 0)}function DS(n){let t={velocity:Me.velocity,stiffness:Me.stiffness,damping:Me.damping,mass:Me.mass,isResolvedFromDuration:!1,...n};if(!gy(n,IS)&&gy(n,OS))if(t.velocity=0,n.visualDuration){const e=n.visualDuration,i=2*Math.PI/(e*1.2),a=i*i,o=2*Ei(.05,1,1-(n.bounce||0))*Math.sqrt(a);t={...t,mass:Me.mass,stiffness:a,damping:o}}else{const e=NS({...n,velocity:0});t={...t,...e,mass:Me.mass},t.isResolvedFromDuration=!0}return t}function Il(n=Me.visualDuration,t=Me.bounce){const e=typeof n!="object"?{visualDuration:n,keyframes:[0,1],bounce:t}:n;let{restSpeed:i,restDelta:a}=e;const o=e.keyframes[0],c=e.keyframes[e.keyframes.length-1],u={done:!1,value:o},{stiffness:f,damping:d,mass:m,duration:g,velocity:v,isResolvedFromDuration:x}=DS({...e,velocity:-Xn(e.velocity||0)}),b=v||0,C=d/(2*Math.sqrt(f*m)),T=c-o,S=Xn(Math.sqrt(f/m)),L=Math.abs(T)<5;i||(i=L?Me.restSpeed.granular:Me.restSpeed.default),a||(a=L?Me.restDelta.granular:Me.restDelta.default);let R,P,w,F,E,O;if(C<1)w=X0(S,C),F=(b+C*S*T)/w,R=k=>{const U=Math.exp(-C*S*k);return c-U*(F*Math.sin(w*k)+T*Math.cos(w*k))},E=C*S*F+T*w,O=C*S*T-F*w,P=k=>Math.exp(-C*S*k)*(E*Math.sin(w*k)+O*Math.cos(w*k));else if(C===1){R=U=>c-Math.exp(-S*U)*(T+(b+S*T)*U);const k=b+S*T;P=U=>Math.exp(-S*U)*(S*k*U-b)}else{const k=S*Math.sqrt(C*C-1);R=tt=>{const K=Math.exp(-C*S*tt),A=Math.min(k*tt,300);return c-K*((b+C*S*T)*Math.sinh(A)+k*T*Math.cosh(A))/k};const U=(b+C*S*T)/k,G=C*S*U-T*k,J=C*S*T-U*k;P=tt=>{const K=Math.exp(-C*S*tt),A=Math.min(k*tt,300);return K*(G*Math.sinh(A)+J*Math.cosh(A))}}const I={calculatedDuration:x&&g||null,velocity:k=>Cn(P(k)),next:k=>{if(!x&&C<1){const G=Math.exp(-C*S*k),J=Math.sin(w*k),tt=Math.cos(w*k),K=c-G*(F*J+T*tt),A=Cn(G*(E*J+O*tt));return u.done=Math.abs(A)<=i&&Math.abs(c-K)<=a,u.value=u.done?c:K,u}const U=R(k);if(x)u.done=k>=g;else{const G=Cn(P(k));u.done=Math.abs(G)<=i&&Math.abs(c-U)<=a}return u.value=u.done?c:U,u},toString:()=>{const k=Math.min(e2(I),eh),U=S7(G=>I.next(k*G).value,k,30);return k+"ms "+U},toTransition:()=>{}};return I}Il.applyToOptions=n=>{const t=_7(n,100,Il);return n.ease=t.ease,n.duration=Cn(t.duration),n.type="keyframes",n};const BS=5;function A7(n,t,e){const i=Math.max(t-BS,0);return n7(e-n(i),t-i)}function Y0({keyframes:n,velocity:t=0,power:e=.8,timeConstant:i=325,bounceDamping:a=10,bounceStiffness:o=500,modifyTarget:c,min:u,max:f,restDelta:d=.5,restSpeed:m}){const g=n[0],v={done:!1,value:g},x=O=>u!==void 0&&Of,b=O=>u===void 0?f:f===void 0||Math.abs(u-O)-C*Math.exp(-O/i),R=O=>S+L(O),P=O=>{const I=L(O),k=R(O);v.done=Math.abs(I)<=d,v.value=v.done?S:k};let w,F;const E=O=>{x(v.value)&&(w=O,F=Il({keyframes:[v.value,b(v.value)],velocity:A7(R,O,v.value),damping:a,stiffness:o,restDelta:d,restSpeed:m}))};return E(0),{calculatedDuration:null,next:O=>{let I=!1;return!F&&w===void 0&&(I=!0,P(O),E(O)),w!==void 0&&O>=w?F.next(O-w):(!I&&P(O),v)}}}function PS(n,t,e){const i=[],a=e||zs.mix||C7,o=n.length-1;for(let c=0;ct[0];if(o===2&&t[0]===t[1])return()=>t[1];const c=n[0]===n[1];n[0]>n[o-1]&&(n=[...n].reverse(),t=[...t].reverse());const u=PS(t,i,a),f=u.length,d=m=>{if(c&&m1)for(;gd(Ei(n[0],n[o-1],m)):d}function w7(n,t){const e=n[n.length-1];for(let i=1;i<=t;i++){const a=Xa(0,t,i);n.push(ye(e,1,a))}}function M7(n){const t=[0];return w7(t,n.length-1),t}function FS(n,t){return n.map(e=>e*t)}function US(n,t){return n.map(()=>t||u7).splice(0,n.length-1)}function Cl({duration:n=300,keyframes:t,times:e,ease:i="easeInOut"}){const a=h7(i)?i.map(uy):uy(i),o={done:!1,value:t[0]},c=FS(e&&e.length===t.length?e:M7(t),n),u=E7(c,t,{ease:Array.isArray(a)?a:US(t,a)});return{calculatedDuration:n,next:f=>(o.value=u(f),o.done=f>=n,o)}}const kS=n=>n!==null;function Th(n,{repeat:t,repeatType:e="loop"},i,a=1){const o=n.filter(kS),u=a<0||t&&e!=="loop"&&t%2===1?0:o.length-1;return!u||i===void 0?o[u]:i}const zS={decay:Y0,inertia:Y0,tween:Cl,keyframes:Cl,spring:Il};function R7(n){typeof n.type=="string"&&(n.type=zS[n.type])}class n2{constructor(){this.updateFinished()}get finished(){return this._finished}updateFinished(){this._finished=new Promise(t=>{this.resolve=t})}notifyFinished(){this.resolve()}then(t,e){return this.finished.then(t,e)}}const VS=n=>n/100;class Dl extends n2{constructor(t){super(),this.state="idle",this.startTime=null,this.isStopped=!1,this.currentTime=0,this.holdTime=null,this.playbackSpeed=1,this.delayState={done:!1,value:void 0},this.stop=()=>{const{motionValue:e}=this.options;e&&e.updatedAt!==dn.now()&&this.tick(dn.now()),this.isStopped=!0,this.state!=="idle"&&(this.teardown(),this.options.onStop?.())},this.options=t,this.initAnimation(),this.play(),t.autoplay===!1&&this.pause()}initAnimation(){const{options:t}=this;R7(t);const{type:e=Cl,repeat:i=0,repeatDelay:a=0,repeatType:o,velocity:c=0}=t;let{keyframes:u}=t;const f=e||Cl;f!==Cl&&typeof u[0]!="number"&&(this.mixKeyframes=Hl(VS,C7(u[0],u[1])),u=[0,100]);const d=f({...t,keyframes:u});o==="mirror"&&(this.mirroredGenerator=f({...t,keyframes:[...u].reverse(),velocity:-c})),d.calculatedDuration===null&&(d.calculatedDuration=e2(d));const{calculatedDuration:m}=d;this.calculatedDuration=m,this.resolvedDuration=m+a,this.totalDuration=this.resolvedDuration*(i+1)-a,this.generator=d}updateTime(t){const e=Math.round(t-this.startTime)*this.playbackSpeed;this.holdTime!==null?this.currentTime=this.holdTime:this.currentTime=e}tick(t,e=!1){const{generator:i,totalDuration:a,mixKeyframes:o,mirroredGenerator:c,resolvedDuration:u,calculatedDuration:f}=this;if(this.startTime===null)return i.next(0);const{delay:d=0,keyframes:m,repeat:g,repeatType:v,repeatDelay:x,type:b,onUpdate:C,finalKeyframe:T}=this.options;this.speed>0?this.startTime=Math.min(this.startTime,t):this.speed<0&&(this.startTime=Math.min(t-a/this.speed,this.startTime)),e?this.currentTime=t:this.updateTime(t);const S=this.currentTime-d*(this.playbackSpeed>=0?1:-1),L=this.playbackSpeed>=0?S<0:S>a;this.currentTime=Math.max(S,0),this.state==="finished"&&this.holdTime===null&&(this.currentTime=a);let R=this.currentTime,P=i;if(g){const O=Math.min(this.currentTime,a)/u;let I=Math.floor(O),k=O%1;!k&&O>=1&&(k=1),k===1&&I--,I=Math.min(I,g+1),I%2&&(v==="reverse"?(k=1-k,x&&(k-=x/u)):v==="mirror"&&(P=c)),R=Ei(0,1,k)*u}let w;L?(this.delayState.value=m[0],w=this.delayState):w=P.next(R),o&&!L&&(w.value=o(w.value));let{done:F}=w;!L&&f!==null&&(F=this.playbackSpeed>=0?this.currentTime>=a:this.currentTime<=0);const E=this.holdTime===null&&(this.state==="finished"||this.state==="running"&&F);return E&&b!==Y0&&(w.value=Th(m,this.options,T,this.speed)),C&&C(w.value),E&&this.finish(),w}then(t,e){return this.finished.then(t,e)}get duration(){return Xn(this.calculatedDuration)}get iterationDuration(){const{delay:t=0}=this.options||{};return this.duration+Xn(t)}get time(){return Xn(this.currentTime)}set time(t){t=Cn(t),this.currentTime=t,this.startTime===null||this.holdTime!==null||this.playbackSpeed===0?this.holdTime=t:this.driver&&(this.startTime=this.driver.now()-t/this.playbackSpeed),this.driver?this.driver.start(!1):(this.startTime=0,this.state="paused",this.holdTime=t,this.tick(t))}getGeneratorVelocity(){const t=this.currentTime;if(t<=0)return this.options.velocity||0;if(this.generator.velocity)return this.generator.velocity(t);const e=this.generator.next(t).value;return A7(i=>this.generator.next(i).value,t,e)}get speed(){return this.playbackSpeed}set speed(t){const e=this.playbackSpeed!==t;e&&this.driver&&this.updateTime(dn.now()),this.playbackSpeed=t,e&&this.driver&&(this.time=Xn(this.currentTime))}play(){if(this.isStopped)return;const{driver:t=MS,startTime:e}=this.options;this.driver||(this.driver=t(a=>this.tick(a))),this.options.onPlay?.();const i=this.driver.now();this.state==="finished"?(this.updateFinished(),this.startTime=i):this.holdTime!==null?this.startTime=i-this.holdTime:this.startTime||(this.startTime=e??i),this.state==="finished"&&this.speed<0&&(this.startTime+=this.calculatedDuration),this.holdTime=null,this.state="running",this.driver.start()}pause(){this.state="paused",this.updateTime(dn.now()),this.holdTime=this.currentTime}complete(){this.state!=="running"&&this.play(),this.state="finished",this.holdTime=null}finish(){this.notifyFinished(),this.teardown(),this.state="finished",this.options.onComplete?.()}cancel(){this.holdTime=null,this.startTime=0,this.tick(0),this.teardown(),this.options.onCancel?.()}teardown(){this.state="idle",this.stopDriver(),this.startTime=this.holdTime=null}stopDriver(){this.driver&&(this.driver.stop(),this.driver=void 0)}sample(t){return this.startTime=0,this.tick(t,!0)}attachTimeline(t){return this.options.allowFlatten&&(this.options.type="keyframes",this.options.ease="linear",this.initAnimation()),this.driver?.stop(),t.observe(this)}}function GS(n){for(let t=1;tn*180/Math.PI,$0=n=>{const t=Ar(Math.atan2(n[1],n[0]));return W0(t)},jS={x:4,y:5,translateX:4,translateY:5,scaleX:0,scaleY:3,scale:n=>(Math.abs(n[0])+Math.abs(n[3]))/2,rotate:$0,rotateZ:$0,skewX:n=>Ar(Math.atan(n[1])),skewY:n=>Ar(Math.atan(n[2])),skew:n=>(Math.abs(n[1])+Math.abs(n[2]))/2},W0=n=>(n=n%360,n<0&&(n+=360),n),yy=$0,vy=n=>Math.sqrt(n[0]*n[0]+n[1]*n[1]),by=n=>Math.sqrt(n[4]*n[4]+n[5]*n[5]),HS={x:12,y:13,z:14,translateX:12,translateY:13,translateZ:14,scaleX:vy,scaleY:by,scale:n=>(vy(n)+by(n))/2,rotateX:n=>W0(Ar(Math.atan2(n[6],n[5]))),rotateY:n=>W0(Ar(Math.atan2(-n[2],n[0]))),rotateZ:yy,rotate:yy,skewX:n=>Ar(Math.atan(n[4])),skewY:n=>Ar(Math.atan(n[1])),skew:n=>(Math.abs(n[1])+Math.abs(n[4]))/2};function Z0(n){return n.includes("scale")?1:0}function K0(n,t){if(!n||n==="none")return Z0(t);const e=n.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);let i,a;if(e)i=HS,a=e;else{const u=n.match(/^matrix\(([-\d.e\s,]+)\)$/u);i=jS,a=u}if(!a)return Z0(t);const o=i[t],c=a[1].split(",").map(XS);return typeof o=="function"?o(c):c[o]}const qS=(n,t)=>{const{transform:e="none"}=getComputedStyle(n);return K0(e,t)};function XS(n){return parseFloat(n.trim())}const Ka=["transformPerspective","x","y","z","translateX","translateY","translateZ","scale","scaleX","scaleY","rotate","rotateX","rotateY","rotateZ","skew","skewX","skewY"],Qa=new Set([...Ka,"pathRotation"]),xy=n=>n===Za||n===_t,YS=new Set(["x","y","z"]),$S=Ka.filter(n=>!YS.has(n));function WS(n){const t=[];return $S.forEach(e=>{const i=n.getValue(e);i!==void 0&&(t.push([e,i.get()]),i.set(e.startsWith("scale")?1:0))}),t}const Ps={width:({x:n},{paddingLeft:t="0",paddingRight:e="0",boxSizing:i})=>{const a=n.max-n.min;return i==="border-box"?a:a-parseFloat(t)-parseFloat(e)},height:({y:n},{paddingTop:t="0",paddingBottom:e="0",boxSizing:i})=>{const a=n.max-n.min;return i==="border-box"?a:a-parseFloat(t)-parseFloat(e)},top:(n,{top:t})=>parseFloat(t),left:(n,{left:t})=>parseFloat(t),bottom:({y:n},{top:t})=>parseFloat(t)+(n.max-n.min),right:({x:n},{left:t})=>parseFloat(t)+(n.max-n.min),x:(n,{transform:t})=>K0(t,"x"),y:(n,{transform:t})=>K0(t,"y")};Ps.translateX=Ps.x;Ps.translateY=Ps.y;const Mr=new Set;let Q0=!1,J0=!1,tm=!1;function L7(){if(J0){const n=Array.from(Mr).filter(i=>i.needsMeasurement),t=new Set(n.map(i=>i.element)),e=new Map;t.forEach(i=>{const a=WS(i);a.length&&(e.set(i,a),i.render())}),n.forEach(i=>i.measureInitialState()),t.forEach(i=>{i.render();const a=e.get(i);a&&a.forEach(([o,c])=>{i.getValue(o)?.set(c)})}),n.forEach(i=>i.measureEndState()),n.forEach(i=>{i.suspendedScrollY!==void 0&&window.scrollTo(0,i.suspendedScrollY)})}J0=!1,Q0=!1,Mr.forEach(n=>n.complete(tm)),Mr.clear()}function N7(){Mr.forEach(n=>{n.readKeyframes(),n.needsMeasurement&&(J0=!0)})}function ZS(){tm=!0,N7(),L7(),tm=!1}class i2{constructor(t,e,i,a,o,c=!1){this.state="pending",this.isAsync=!1,this.needsMeasurement=!1,this.unresolvedKeyframes=[...t],this.onComplete=e,this.name=i,this.motionValue=a,this.element=o,this.isAsync=c}scheduleResolve(){this.state="scheduled",this.isAsync?(Mr.add(this),Q0||(Q0=!0,re.read(N7),re.resolveKeyframes(L7))):(this.readKeyframes(),this.complete())}readKeyframes(){const{unresolvedKeyframes:t,name:e,element:i,motionValue:a}=this;if(t[0]===null){const o=a?.get(),c=t[t.length-1];if(o!==void 0)t[0]=o;else if(i&&e){const u=i.readValue(e,c);u!=null&&(t[0]=u)}t[0]===void 0&&(t[0]=c),a&&o===void 0&&a.set(t[0])}GS(t)}setFinalKeyframe(){}measureInitialState(){}renderEndStyles(){}measureEndState(){}complete(t=!1){this.state="complete",this.onComplete(this.unresolvedKeyframes,this.finalKeyframe,t),Mr.delete(this)}cancel(){this.state==="scheduled"&&(Mr.delete(this),this.state="pending")}resume(){this.state==="pending"&&this.scheduleResolve()}}const KS=n=>n.startsWith("--");function O7(n,t,e){KS(t)?n.style.setProperty(t,e):n.style[t]=e}const QS={};function I7(n,t){const e=e7(n);return()=>QS[t]??e()}const JS=I7(()=>window.ScrollTimeline!==void 0,"scrollTimeline"),D7=I7(()=>{try{document.createElement("div").animate({opacity:0},{easing:"linear(0, 1)"})}catch{return!1}return!0},"linearEasing"),fl=([n,t,e,i])=>`cubic-bezier(${n}, ${t}, ${e}, ${i})`,Ty={linear:"linear",ease:"ease",easeIn:"ease-in",easeOut:"ease-out",easeInOut:"ease-in-out",circIn:fl([0,.65,.55,1]),circOut:fl([.55,0,1,.45]),backIn:fl([.31,.01,.66,-.59]),backOut:fl([.33,1.53,.69,.99])};function B7(n,t){if(n)return typeof n=="function"?D7()?S7(n,t):"ease-out":d7(n)?fl(n):Array.isArray(n)?n.map(e=>B7(e,t)||Ty.easeOut):Ty[n]}function t_(n,t,e,{delay:i=0,duration:a=300,repeat:o=0,repeatType:c="loop",ease:u="easeOut",times:f}={},d=void 0){const m={[t]:e};f&&(m.offset=f);const g=B7(u,a);Array.isArray(g)&&(m.easing=g);const v={delay:i,duration:a,easing:Array.isArray(g)?"linear":g,fill:"both",iterations:o+1,direction:c==="reverse"?"alternate":"normal"};return d&&(v.pseudoElement=d),n.animate(m,v)}function s2(n){return typeof n=="function"&&"applyToOptions"in n}function e_({type:n,...t}){return s2(n)&&D7()?n.applyToOptions(t):(t.duration??(t.duration=300),t.ease??(t.ease="easeOut"),t)}class P7 extends n2{constructor(t){if(super(),this.finishedTime=null,this.isStopped=!1,this.manualStartTime=null,!t)return;const{element:e,name:i,keyframes:a,pseudoElement:o,allowFlatten:c=!1,finalKeyframe:u,onComplete:f}=t;this.isPseudoElement=!!o,this.allowFlatten=c,this.options=t,xh(typeof t.type!="string");const d=e_(t);this.animation=t_(e,i,a,d,o),d.autoplay===!1&&this.animation.pause(),this.animation.onfinish=()=>{if(this.finishedTime=this.time,!o){const m=Th(a,this.options,u,this.speed);this.updateMotionValue&&this.updateMotionValue(m),O7(e,i,m),this.animation.cancel()}f?.(),this.notifyFinished()}}play(){this.isStopped||(this.manualStartTime=null,this.animation.play(),this.state==="finished"&&this.updateFinished())}pause(){this.animation.pause()}complete(){this.animation.finish?.()}cancel(){try{this.animation.cancel()}catch{}}stop(){if(this.isStopped)return;this.isStopped=!0;const{state:t}=this;t==="idle"||t==="finished"||(this.updateMotionValue?this.updateMotionValue():this.commitStyles(),this.isPseudoElement||this.cancel())}commitStyles(){const t=this.options?.element;!this.isPseudoElement&&t?.isConnected&&this.animation.commitStyles?.()}get duration(){const t=this.animation.effect?.getComputedTiming?.().duration||0;return Xn(Number(t))}get iterationDuration(){const{delay:t=0}=this.options||{};return this.duration+Xn(t)}get time(){return Xn(Number(this.animation.currentTime)||0)}set time(t){const e=this.finishedTime!==null;this.manualStartTime=null,this.finishedTime=null,this.animation.currentTime=Cn(t),e&&this.animation.pause()}get speed(){return this.animation.playbackRate}set speed(t){t<0&&(this.finishedTime=null),this.animation.playbackRate=t}get state(){return this.finishedTime!==null?"finished":this.animation.playState}get startTime(){return this.manualStartTime??Number(this.animation.startTime)}set startTime(t){this.manualStartTime=this.animation.startTime=t}attachTimeline({timeline:t,rangeStart:e,rangeEnd:i,observe:a}){return this.allowFlatten&&this.animation.effect?.updateTiming({easing:"linear"}),this.animation.onfinish=null,t&&JS()?(this.animation.timeline=t,e&&(this.animation.rangeStart=e),i&&(this.animation.rangeEnd=i),Zn):a(this)}}const F7={anticipate:o7,backInOut:a7,circInOut:c7};function n_(n){return n in F7}function i_(n){typeof n.ease=="string"&&n_(n.ease)&&(n.ease=F7[n.ease])}const cd=10;class s_ extends P7{constructor(t){i_(t),R7(t),super(t),t.startTime!==void 0&&t.autoplay!==!1&&(this.startTime=t.startTime),this.options=t}updateMotionValue(t){const{motionValue:e,onUpdate:i,onComplete:a,element:o,...c}=this.options;if(!e)return;if(t!==void 0){e.set(t);return}const u=new Dl({...c,autoplay:!1}),f=Math.max(cd,dn.now()-this.startTime),d=Ei(0,cd,f-cd),m=u.sample(f).value,{name:g}=this.options;o&&g&&O7(o,g,m),e.setWithVelocity(u.sample(Math.max(0,f-d)).value,m,d),u.stop()}}const Cy=(n,t)=>t==="zIndex"?!1:!!(typeof n=="number"||Array.isArray(n)||typeof n=="string"&&(ci.test(n)||n==="0")&&!n.startsWith("url("));function r_(n){const t=n[0];if(n.length===1)return!0;for(let e=0;eObject.hasOwnProperty.call(Element.prototype,"animate"));function h_(n){const{motionValue:t,name:e,repeatDelay:i,repeatType:a,damping:o,type:c,keyframes:u}=n,f=t?.owner?.current;if(!(f instanceof HTMLElement)&&!(f instanceof SVGElement))return!1;const{onUpdate:d,transformTemplate:m}=t.owner.getProps();return u_()&&e&&(U7.has(e)||c_.has(e)&&l_(u))&&(e!=="transform"||!m)&&!d&&!i&&a!=="mirror"&&o!==0&&c!=="inertia"}const f_=40;class d_ extends n2{constructor({autoplay:t=!0,delay:e=0,type:i="keyframes",repeat:a=0,repeatDelay:o=0,repeatType:c="loop",keyframes:u,name:f,motionValue:d,element:m,...g}){super(),this.stop=()=>{this._animation&&(this._animation.stop(),this.stopTimeline?.()),this.keyframeResolver?.cancel()},this.createdAt=dn.now();const v={autoplay:t,delay:e,type:i,repeat:a,repeatDelay:o,repeatType:c,name:f,motionValue:d,element:m,...g},x=m?.KeyframeResolver||i2;this.keyframeResolver=new x(u,(b,C,T)=>this.onKeyframesResolved(b,C,v,!T),f,d,m),this.keyframeResolver?.scheduleResolve()}onKeyframesResolved(t,e,i,a){this.keyframeResolver=void 0;const{name:o,type:c,velocity:u,delay:f,isHandoff:d,onUpdate:m}=i;this.resolvedAt=dn.now();let g=!0;a_(t,o,c,u)||(g=!1,(zs.instantAnimations||!f)&&m?.(Th(t,i,e)),t[0]=t[t.length-1],em(i),i.repeat=0);const x={startTime:a?this.resolvedAt?this.resolvedAt-this.createdAt>f_?this.resolvedAt:this.createdAt:this.createdAt:void 0,finalKeyframe:e,...i,keyframes:t},b=g&&!d&&h_(x),C=x.motionValue?.owner?.current;let T;if(b)try{T=new s_({...x,element:C})}catch{T=new Dl(x)}else T=new Dl(x);T.finished.then(()=>{this.notifyFinished()}).catch(Zn),this.pendingTimeline&&(this.stopTimeline=T.attachTimeline(this.pendingTimeline),this.pendingTimeline=void 0),this._animation=T}get finished(){return this._animation?this.animation.finished:this._finished}then(t,e){return this.finished.finally(t).then(()=>{})}get animation(){return this._animation||(this.keyframeResolver?.resume(),ZS()),this._animation}get duration(){return this.animation.duration}get iterationDuration(){return this.animation.iterationDuration}get time(){return this.animation.time}set time(t){this.animation.time=t}get speed(){return this.animation.speed}get state(){return this.animation.state}set speed(t){this.animation.speed=t}get startTime(){return this.animation.startTime}attachTimeline(t){return this._animation?this.stopTimeline=this.animation.attachTimeline(t):this.pendingTimeline=t,()=>this.stop()}play(){this.animation.play()}pause(){this.animation.pause()}complete(){this.animation.complete()}cancel(){this._animation&&this.animation.cancel(),this.keyframeResolver?.cancel()}}class m_{constructor(t){this.stop=()=>this.runAll("stop"),this.animations=t.filter(Boolean)}get finished(){return Promise.all(this.animations.map(t=>t.finished))}getAll(t){return this.animations[0][t]}setAll(t,e){for(let i=0;ii.attachTimeline(t));return()=>{e.forEach((i,a)=>{i&&i(),this.animations[a].stop()})}}get time(){return this.getAll("time")}set time(t){this.setAll("time",t)}get speed(){return this.getAll("speed")}set speed(t){this.setAll("speed",t)}get state(){return this.getAll("state")}get startTime(){return this.getAll("startTime")}get duration(){return Sy(this.animations,"duration")}get iterationDuration(){return Sy(this.animations,"iterationDuration")}runAll(t){this.animations.forEach(e=>e[t]())}play(){this.runAll("play")}pause(){this.runAll("pause")}cancel(){this.runAll("cancel")}complete(){this.runAll("complete")}}function Sy(n,t){let e=0;for(let i=0;ie&&(e=a)}return e}class p_ extends m_{then(t,e){return this.finished.finally(t).then(()=>{})}}function k7(n,t,e,i=0,a=1){const o=Array.from(n).sort((d,m)=>d.sortNodePosition(m)).indexOf(t),c=n.size,u=(c-1)*i;return typeof e=="function"?e(o,c):a===1?o*i:u-o*i}const _y=30,g_=n=>!isNaN(parseFloat(n)),Sl={current:void 0};class y_{constructor(t,e={}){this.canTrackVelocity=null,this.events={},this.updateAndNotify=i=>{const a=dn.now();if(this.updatedAt!==a&&this.setPrevFrameValue(),this.prev=this.current,this.setCurrent(i),this.current!==this.prev&&(this.events.change?.notify(this.current),this.dependents))for(const o of this.dependents)o.dirty()},this.hasAnimated=!1,this.setCurrent(t),this.owner=e.owner}setCurrent(t){this.current=t,this.updatedAt=dn.now(),this.canTrackVelocity===null&&t!==void 0&&(this.canTrackVelocity=g_(this.current))}setPrevFrameValue(t=this.current){this.prevFrameValue=t,this.prevUpdatedAt=this.updatedAt}onChange(t){return this.on("change",t)}on(t,e){this.events[t]||(this.events[t]=new Ym);const i=this.events[t].add(e);return t==="change"?()=>{i(),re.read(()=>{this.events.change.getSize()||this.stop()})}:i}clearListeners(){for(const t in this.events)this.events[t].clear()}attach(t,e){this.passiveEffect=t,this.stopPassiveEffect=e}set(t){this.passiveEffect?this.passiveEffect(t,this.updateAndNotify):this.updateAndNotify(t)}setWithVelocity(t,e,i){this.set(e),this.prev=void 0,this.prevFrameValue=t,this.prevUpdatedAt=this.updatedAt-i}jump(t,e=!0){this.updateAndNotify(t),this.prev=t,this.prevUpdatedAt=this.prevFrameValue=void 0,e&&this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}dirty(){this.events.change?.notify(this.current)}addDependent(t){this.dependents||(this.dependents=new Set),this.dependents.add(t)}removeDependent(t){this.dependents&&this.dependents.delete(t)}get(){return Sl.current&&Sl.current.push(this),this.current}getPrevious(){return this.prev}getVelocity(){const t=dn.now();if(!this.canTrackVelocity||this.prevFrameValue===void 0||t-this.updatedAt>_y)return 0;const e=Math.min(this.updatedAt-this.prevUpdatedAt,_y);return n7(parseFloat(this.current)-parseFloat(this.prevFrameValue),e)}start(t){return this.stop(),new Promise(e=>{this.hasAnimated=!0,this.animation=t(e),this.events.animationStart&&this.events.animationStart.notify()}).then(()=>{this.events.animationComplete&&this.events.animationComplete.notify(),this.clearAnimation()})}stop(){this.animation&&(this.animation.stop(),this.events.animationCancel&&this.events.animationCancel.notify()),this.clearAnimation()}isAnimating(){return!!this.animation}clearAnimation(){delete this.animation}destroy(){this.dependents?.clear(),this.events.destroy?.notify(),this.clearListeners(),this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}}function Vs(n,t){return new y_(n,t)}function z7(n,t){if(n?.inherit&&t){const{inherit:e,...i}=n;return{...t,...i}}return n}function r2(n,t){const e=n?.[t]??n?.default??n;return e!==n?z7(e,n):e}const v_={type:"spring",stiffness:500,damping:25,restSpeed:10},b_=n=>({type:"spring",stiffness:550,damping:n===0?2*Math.sqrt(550):30,restSpeed:10}),x_={type:"keyframes",duration:.8},T_={type:"keyframes",ease:[.25,.1,.35,1],duration:.3},C_=(n,{keyframes:t})=>t.length>2?x_:Qa.has(n)?n.startsWith("scale")?b_(t[1]):v_:T_,S_=new Set(["when","delay","delayChildren","staggerChildren","staggerDirection","repeat","repeatType","repeatDelay","from","elapsed"]);function __(n){for(const t in n)if(!S_.has(t))return!0;return!1}const a2=(n,t,e,i={},a,o)=>c=>{const u=r2(i,n)||{},f=u.delay||i.delay||0;let{elapsed:d=0}=i;d=d-Cn(f);const m={keyframes:Array.isArray(e)?e:[null,e],ease:"easeOut",velocity:t.getVelocity(),...u,delay:-d,onUpdate:v=>{t.set(v),u.onUpdate&&u.onUpdate(v)},onComplete:()=>{c(),u.onComplete&&u.onComplete()},name:n,motionValue:t,element:o?void 0:a};__(u)||Object.assign(m,C_(n,m)),m.duration&&(m.duration=Cn(m.duration)),m.repeatDelay&&(m.repeatDelay=Cn(m.repeatDelay)),m.from!==void 0&&(m.keyframes[0]=m.from);let g=!1;if((m.type===!1||m.duration===0&&!m.repeatDelay)&&(em(m),m.delay===0&&(g=!0)),(zs.instantAnimations||zs.skipAnimations||a?.shouldSkipAnimations||u.skipAnimations)&&(g=!0,em(m),m.delay=0),m.allowFlatten=!u.type&&!u.ease,g&&!o&&t.get()!==void 0){const v=Th(m.keyframes,u);if(v!==void 0){re.update(()=>{m.onUpdate(v),m.onComplete()});return}}return u.isSync?new Dl(m):new d_(m)},A_=/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;function E_(n){const t=A_.exec(n);if(!t)return[,];const[,e,i,a]=t;return[`--${e??i}`,a]}function V7(n,t,e=1){const[i,a]=E_(n);if(!i)return;const o=window.getComputedStyle(t).getPropertyValue(i);if(o){const c=o.trim();return Q8(c)?parseFloat(c):c}return Km(a)?V7(a,t,e+1):a}function Ay(n){const t=[{},{}];return n?.values.forEach((e,i)=>{t[0][i]=e.get(),t[1][i]=e.getVelocity()}),t}function o2(n,t,e,i){if(typeof t=="function"){const[a,o]=Ay(i);t=t(e!==void 0?e:n.custom,a,o)}if(typeof t=="string"&&(t=n.variants&&n.variants[t]),typeof t=="function"){const[a,o]=Ay(i);t=t(e!==void 0?e:n.custom,a,o)}return t}function Rr(n,t,e){const i=n.getProps();return o2(i,t,e!==void 0?e:i.custom,n)}const G7=new Set(["width","height","top","left","right","bottom",...Ka]),nm=n=>Array.isArray(n);function w_(n,t,e){n.hasValue(t)?n.getValue(t).set(e):n.addValue(t,Vs(e))}function M_(n){return nm(n)?n[n.length-1]||0:n}function R_(n,t){const e=Rr(n,t);let{transitionEnd:i={},transition:a={},...o}=e||{};o={...o,...i};for(const c in o){const u=M_(o[c]);w_(n,c,u)}}const Ae=n=>!!(n&&n.getVelocity);function L_(n){return!!(Ae(n)&&n.add)}function im(n,t){const e=n.getValue("willChange");if(L_(e))return e.add(t);if(!e&&zs.WillChange){const i=new zs.WillChange("auto");n.addValue("willChange",i),i.add(t)}}function l2(n){return n.replace(/([A-Z])/g,t=>`-${t.toLowerCase()}`)}const N_="framerAppearId",j7="data-"+l2(N_);function H7(n){return n.props[j7]}function O_({protectedKeys:n,needsAnimating:t},e){const i=n.hasOwnProperty(e)&&t[e]!==!0;return t[e]=!1,i}function c2(n,t,{delay:e=0,transitionOverride:i,type:a}={}){let{transition:o,transitionEnd:c,...u}=t;const f=n.getDefaultTransition();o=o?z7(o,f):f;const d=o?.reduceMotion,m=o?.skipAnimations;i&&(o=i);const g=[],v=a&&n.animationState&&n.animationState.getState()[a],x=o?.path;x&&x.animateVisualElement(n,u,o,e,g);for(const b in u){const C=n.getValue(b,n.latestValues[b]??null),T=u[b];if(T===void 0||v&&O_(v,b))continue;const S={delay:e,...r2(o||{},b)};m&&(S.skipAnimations=!0);const L=C.get();if(L!==void 0&&!C.isAnimating()&&!Array.isArray(T)&&T===L&&!S.velocity){re.update(()=>C.set(T));continue}let R=!1;if(window.MotionHandoffAnimation){const F=H7(n);if(F){const E=window.MotionHandoffAnimation(F,b,re);E!==null&&(S.startTime=E,R=!0)}}im(n,b);const P=d??n.shouldReduceMotion;C.start(a2(b,C,T,P&&G7.has(b)?{type:!1}:S,n,R));const w=C.animation;w&&g.push(w)}if(c){const b=()=>re.update(()=>{c&&R_(n,c)});g.length?Promise.all(g).then(b):b()}return g}function sm(n,t,e={}){const i=Rr(n,t,e.type==="exit"?n.presenceContext?.custom:void 0);let{transition:a=n.getDefaultTransition()||{}}=i||{};e.transitionOverride&&(a=e.transitionOverride);const o=i?()=>Promise.all(c2(n,i,e)):()=>Promise.resolve(),c=n.variantChildren&&n.variantChildren.size?(f=0)=>{const{delayChildren:d=0,staggerChildren:m,staggerDirection:g}=a;return I_(n,t,f,d,m,g,e)}:()=>Promise.resolve(),{when:u}=a;if(u){const[f,d]=u==="beforeChildren"?[o,c]:[c,o];return f().then(()=>d())}else return Promise.all([o(),c(e.delay)])}function I_(n,t,e=0,i=0,a=0,o=1,c){const u=[];for(const f of n.variantChildren)f.notify("AnimationStart",t),u.push(sm(f,t,{...c,delay:e+(typeof i=="function"?0:i)+k7(n.variantChildren,f,i,a,o)}).then(()=>f.notify("AnimationComplete",t)));return Promise.all(u)}function D_(n,t,e={}){n.notify("AnimationStart",t);let i;if(Array.isArray(t)){const a=t.map(o=>sm(n,o,e));i=Promise.all(a)}else if(typeof t=="string")i=sm(n,t,e);else{const a=typeof t=="function"?Rr(n,t,e.custom):t;i=Promise.all(c2(n,a,e))}return i.then(()=>{n.notify("AnimationComplete",t)})}const B_={test:n=>n==="auto",parse:n=>n},q7=n=>t=>t.test(n),X7=[Za,_t,Si,Qi,cS,lS,B_],Ey=n=>X7.find(q7(n));function P_(n){return typeof n=="number"?n===0:n!==null?n==="none"||n==="0"||t7(n):!0}const F_=new Set(["brightness","contrast","saturate","opacity"]);function U_(n){const[t,e]=n.slice(0,-1).split("(");if(t==="drop-shadow")return n;const[i]=e.match(Qm)||[];if(!i)return n;const a=e.replace(i,"");let o=F_.has(t)?1:0;return i!==e&&(o*=100),t+"("+o+a+")"}const k_=/\b([a-z-]*)\(.*?\)/gu,rm={...ci,getAnimatableNone:n=>{const t=n.match(k_);return t?t.map(U_).join(" "):n}},am={...ci,getAnimatableNone:n=>{const t=ci.parse(n);return ci.createTransformer(n)(t.map(i=>typeof i=="number"?0:typeof i=="object"?{...i,alpha:1}:i))}},wy={...Za,transform:Math.round},z_={rotate:Qi,pathRotation:Qi,rotateX:Qi,rotateY:Qi,rotateZ:Qi,scale:fu,scaleX:fu,scaleY:fu,scaleZ:fu,skew:Qi,skewX:Qi,skewY:Qi,distance:_t,translateX:_t,translateY:_t,translateZ:_t,x:_t,y:_t,z:_t,perspective:_t,transformPerspective:_t,opacity:Ol,originX:fy,originY:fy,originZ:_t},nh={borderWidth:_t,borderTopWidth:_t,borderRightWidth:_t,borderBottomWidth:_t,borderLeftWidth:_t,borderRadius:_t,borderTopLeftRadius:_t,borderTopRightRadius:_t,borderBottomRightRadius:_t,borderBottomLeftRadius:_t,width:_t,maxWidth:_t,height:_t,maxHeight:_t,top:_t,right:_t,bottom:_t,left:_t,inset:_t,insetBlock:_t,insetBlockStart:_t,insetBlockEnd:_t,insetInline:_t,insetInlineStart:_t,insetInlineEnd:_t,padding:_t,paddingTop:_t,paddingRight:_t,paddingBottom:_t,paddingLeft:_t,paddingBlock:_t,paddingBlockStart:_t,paddingBlockEnd:_t,paddingInline:_t,paddingInlineStart:_t,paddingInlineEnd:_t,margin:_t,marginTop:_t,marginRight:_t,marginBottom:_t,marginLeft:_t,marginBlock:_t,marginBlockStart:_t,marginBlockEnd:_t,marginInline:_t,marginInlineStart:_t,marginInlineEnd:_t,fontSize:_t,backgroundPositionX:_t,backgroundPositionY:_t,...z_,zIndex:wy,fillOpacity:Ol,strokeOpacity:Ol,numOctaves:wy},V_={...nh,color:Ve,backgroundColor:Ve,outlineColor:Ve,fill:Ve,stroke:Ve,borderColor:Ve,borderTopColor:Ve,borderRightColor:Ve,borderBottomColor:Ve,borderLeftColor:Ve,filter:rm,WebkitFilter:rm,mask:am,WebkitMask:am},Y7=n=>V_[n],G_=new Set([rm,am]);function $7(n,t){let e=Y7(n);return G_.has(e)||(e=ci),e.getAnimatableNone?e.getAnimatableNone(t):void 0}const j_=new Set(["auto","none","0"]);function H_(n,t,e){let i=0,a;for(;i{t.getValue(u).set(f)}),this.resolveNoneKeyframes()}}const u2=["borderTopLeftRadius","borderTopRightRadius","borderBottomRightRadius","borderBottomLeftRadius"];function h2(n,t,e){if(n==null)return[];if(n instanceof EventTarget)return[n];if(typeof n=="string"){let i=document;t&&(i=t.current);const a=e?.[n]??i.querySelectorAll(n);return a?Array.from(a):[]}return Array.from(n).filter(i=>i!=null)}const om=(n,t)=>t&&typeof n=="number"?t.transform(n):n;function Pu(n){return J8(n)&&"offsetHeight"in n&&!("ownerSVGElement"in n)}const{schedule:f2}=m7(queueMicrotask,!1),oi={x:!1,y:!1};function W7(){return oi.x||oi.y}function X_(n){return n==="x"||n==="y"?oi[n]?null:(oi[n]=!0,()=>{oi[n]=!1}):oi.x||oi.y?null:(oi.x=oi.y=!0,()=>{oi.x=oi.y=!1})}function Z7(n,t){const e=h2(n),i=new AbortController,a={passive:!0,...t,signal:i.signal};return[e,a,()=>i.abort()]}function Y_(n){return!(n.pointerType==="touch"||W7())}function $_(n,t,e={}){const[i,a,o]=Z7(n,e);return i.forEach(c=>{let u=!1,f=!1,d;const m=()=>{c.removeEventListener("pointerleave",b)},g=T=>{d&&(d(T),d=void 0),m()},v=T=>{u=!1,window.removeEventListener("pointerup",v),window.removeEventListener("pointercancel",v),f&&(f=!1,g(T))},x=()=>{u=!0,window.addEventListener("pointerup",v,a),window.addEventListener("pointercancel",v,a)},b=T=>{if(T.pointerType!=="touch"){if(u){f=!0;return}g(T)}},C=T=>{if(!Y_(T))return;f=!1;const S=t(c,T);typeof S=="function"&&(d=S,c.addEventListener("pointerleave",b,a))};c.addEventListener("pointerenter",C,a),c.addEventListener("pointerdown",x,a)}),o}const K7=(n,t)=>t?n===t?!0:K7(n,t.parentElement):!1,d2=n=>n.pointerType==="mouse"?typeof n.button!="number"||n.button<=0:n.isPrimary!==!1,W_=new Set(["BUTTON","INPUT","SELECT","TEXTAREA","A"]);function Z_(n){return W_.has(n.tagName)||n.isContentEditable===!0}const K_=new Set(["INPUT","SELECT","TEXTAREA"]);function Q_(n){return K_.has(n.tagName)||n.isContentEditable===!0}const Fu=new WeakSet;function My(n){return t=>{t.key==="Enter"&&n(t)}}function ud(n,t){n.dispatchEvent(new PointerEvent("pointer"+t,{isPrimary:!0,bubbles:!0}))}const J_=(n,t)=>{const e=n.currentTarget;if(!e)return;const i=My(()=>{if(Fu.has(e))return;ud(e,"down");const a=My(()=>{ud(e,"up")}),o=()=>ud(e,"cancel");e.addEventListener("keyup",a,t),e.addEventListener("blur",o,t)});e.addEventListener("keydown",i,t),e.addEventListener("blur",()=>e.removeEventListener("keydown",i),t)};function Ry(n){return d2(n)&&!W7()}const Ly=new WeakSet;function tA(n,t,e={}){const[i,a,o]=Z7(n,e),c=u=>{const f=u.currentTarget;if(!Ry(u)||Ly.has(u))return;Fu.add(f),e.stopPropagation&&Ly.add(u);const d=t(f,u),m={...a,capture:!0},g=(b,C)=>{window.removeEventListener("pointerup",v,m),window.removeEventListener("pointercancel",x,m),Fu.has(f)&&Fu.delete(f),Ry(b)&&typeof d=="function"&&d(b,{success:C})},v=b=>{g(b,f===window||f===document||e.useGlobalTarget||K7(f,b.target))},x=b=>{g(b,!1)};window.addEventListener("pointerup",v,m),window.addEventListener("pointercancel",x,m)};return i.forEach(u=>{(e.useGlobalTarget?window:u).addEventListener("pointerdown",c,a),Pu(u)&&(u.addEventListener("focus",d=>J_(d,a)),!Z_(u)&&!u.hasAttribute("tabindex")&&(u.tabIndex=0))}),o}function Ch(n){return J8(n)&&"ownerSVGElement"in n}const Uu=new WeakMap;let ku;const Q7=(n,t,e)=>(i,a)=>a&&a[0]?a[0][n+"Size"]:Ch(i)&&"getBBox"in i?i.getBBox()[t]:i[e],eA=Q7("inline","width","offsetWidth"),nA=Q7("block","height","offsetHeight");function iA({target:n,borderBoxSize:t}){Uu.get(n)?.forEach(e=>{e(n,{get width(){return eA(n,t)},get height(){return nA(n,t)}})})}function sA(n){n.forEach(iA)}function rA(){typeof ResizeObserver>"u"||(ku=new ResizeObserver(sA))}function aA(n,t){ku||rA();const e=h2(n);return e.forEach(i=>{let a=Uu.get(i);a||(a=new Set,Uu.set(i,a)),a.add(t),ku?.observe(i)}),()=>{e.forEach(i=>{const a=Uu.get(i);a?.delete(t),a?.size||ku?.unobserve(i)})}}const zu=new Set;let Fa;function oA(){Fa=()=>{const n={get width(){return window.innerWidth},get height(){return window.innerHeight}};zu.forEach(t=>t(n))},window.addEventListener("resize",Fa)}function lA(n){return zu.add(n),Fa||oA(),()=>{zu.delete(n),!zu.size&&typeof Fa=="function"&&(window.removeEventListener("resize",Fa),Fa=void 0)}}function Ny(n,t){return typeof n=="function"?lA(n):aA(n,t)}function J7(n){return Ch(n)&&n.tagName==="svg"}function cA(...n){const t=!Array.isArray(n[0]),e=t?0:-1,i=n[0+e],a=n[1+e],o=n[2+e],c=n[3+e],u=E7(a,o,c);return t?u(i):u}function uA(n,t,e={}){const i=n.get();let a=null,o=i,c;const u=typeof i=="string"?i.replace(/[\d.-]/g,""):void 0,f=()=>{a&&(a.stop(),a=null),n.animation=void 0},d=()=>{const g=Oy(n.get()),v=Oy(o);if(g===v){f();return}const x=a?a.getGeneratorVelocity():n.getVelocity();f(),a=new Dl({keyframes:[g,v],velocity:x,type:"spring",restDelta:.001,restSpeed:.01,...e,onUpdate:c})},m=()=>{d(),n.animation=a??void 0,n.events.animationStart?.notify(),a?.then(()=>{n.animation=void 0,n.events.animationComplete?.notify()})};if(n.attach((g,v)=>{o=g,c=x=>v(hd(x,u)),re.postRender(m)},f),Ae(t)){let g=e.skipInitialAnimation===!0;const v=t.on("change",b=>{g?(g=!1,n.jump(hd(b,u),!1)):n.set(hd(b,u))}),x=n.on("destroy",v);return()=>{v(),x()}}return f}function hd(n,t){return t?n+t:n}function Oy(n){return typeof n=="number"?n:parseFloat(n)}const hA=[...X7,Ve,ci],fA=n=>hA.find(q7(n)),Iy=()=>({translate:0,scale:1,origin:0,originPoint:0}),Ua=()=>({x:Iy(),y:Iy()}),Dy=()=>({min:0,max:0}),ze=()=>({x:Dy(),y:Dy()}),Bl=new WeakMap;function Sh(n){return n!==null&&typeof n=="object"&&typeof n.start=="function"}function Pl(n){return typeof n=="string"||Array.isArray(n)}const m2=["animate","whileInView","whileFocus","whileHover","whileTap","whileDrag","exit"],p2=["initial",...m2];function _h(n){return Sh(n.animate)||p2.some(t=>Pl(n[t]))}function t9(n){return!!(_h(n)||n.variants)}function dA(n,t,e){for(const i in t){const a=t[i],o=e[i];if(Ae(a))n.addValue(i,a);else if(Ae(o))n.addValue(i,Vs(a,{owner:n}));else if(o!==a)if(n.hasValue(i)){const c=n.getValue(i);c.liveStyle===!0?c.jump(a):c.hasAnimated||c.set(a)}else{const c=n.getStaticValue(i);n.addValue(i,Vs(c!==void 0?c:a,{owner:n}))}}for(const i in e)t[i]===void 0&&n.removeValue(i);return t}const lm={current:null},e9={current:!1},mA=typeof window<"u";function pA(){if(e9.current=!0,!!mA)if(window.matchMedia){const n=window.matchMedia("(prefers-reduced-motion)"),t=()=>lm.current=n.matches;n.addEventListener("change",t),t()}else lm.current=!1}const By=["AnimationStart","AnimationComplete","Update","BeforeLayoutMeasure","LayoutMeasure","LayoutAnimationStart","LayoutAnimationComplete"];let ih={};function n9(n){ih=n}function gA(){return ih}class i9{scrapeMotionValuesFromProps(t,e,i){return{}}constructor({parent:t,props:e,presenceContext:i,reducedMotionConfig:a,skipAnimations:o,blockInitialAnimation:c,visualState:u},f={}){this.current=null,this.children=new Set,this.isVariantNode=!1,this.isControllingVariants=!1,this.shouldReduceMotion=null,this.shouldSkipAnimations=!1,this.values=new Map,this.KeyframeResolver=i2,this.features={},this.valueSubscriptions=new Map,this.prevMotionValues={},this.hasBeenMounted=!1,this.events={},this.propEventSubscriptions={},this.notifyUpdate=()=>this.notify("Update",this.latestValues),this.render=()=>{this.current&&(this.triggerBuild(),this.renderInstance(this.current,this.renderState,this.props.style,this.projection))},this.renderScheduledAt=0,this.scheduleRender=()=>{const x=dn.now();this.renderScheduledAtthis.bindToMotionValue(i,e)),this.reducedMotionConfig==="never"?this.shouldReduceMotion=!1:this.reducedMotionConfig==="always"?this.shouldReduceMotion=!0:(e9.current||pA(),this.shouldReduceMotion=lm.current),this.shouldSkipAnimations=this.skipAnimationsConfig??!1,this.parent?.addChild(this),this.update(this.props,this.presenceContext),this.hasBeenMounted=!0}unmount(){this.projection&&this.projection.unmount(),wi(this.notifyUpdate),wi(this.render),this.valueSubscriptions.forEach(t=>t()),this.valueSubscriptions.clear(),this.removeFromVariantTree&&this.removeFromVariantTree(),this.parent?.removeChild(this);for(const t in this.events)this.events[t].clear();for(const t in this.features){const e=this.features[t];e&&(e.unmount(),e.isMounted=!1)}this.current=null}addChild(t){this.children.add(t),this.enteringChildren??(this.enteringChildren=new Set),this.enteringChildren.add(t)}removeChild(t){this.children.delete(t),this.enteringChildren&&this.enteringChildren.delete(t)}bindToMotionValue(t,e){if(this.valueSubscriptions.has(t)&&this.valueSubscriptions.get(t)(),e.accelerate&&U7.has(t)&&this.current instanceof HTMLElement){const{factory:c,keyframes:u,times:f,ease:d,duration:m}=e.accelerate,g=new P7({element:this.current,name:t,keyframes:u,times:f,ease:d,duration:Cn(m)}),v=c(g);this.valueSubscriptions.set(t,()=>{v(),g.cancel()});return}const i=Qa.has(t);i&&this.onBindTransform&&this.onBindTransform();const a=e.on("change",c=>{this.latestValues[t]=c,this.props.onUpdate&&re.preRender(this.notifyUpdate),i&&this.projection&&(this.projection.isTransformDirty=!0),this.scheduleRender()});let o;typeof window<"u"&&window.MotionCheckAppearSync&&(o=window.MotionCheckAppearSync(this,t,e)),this.valueSubscriptions.set(t,()=>{a(),o&&o()})}sortNodePosition(t){return!this.current||!this.sortInstanceNodePosition||this.type!==t.type?0:this.sortInstanceNodePosition(this.current,t.current)}updateFeatures(){let t="animation";for(t in ih){const e=ih[t];if(!e)continue;const{isEnabled:i,Feature:a}=e;if(!this.features[t]&&a&&i(this.props)&&(this.features[t]=new a(this)),this.features[t]){const o=this.features[t];o.isMounted?o.update():(o.mount(),o.isMounted=!0)}}}triggerBuild(){this.build(this.renderState,this.latestValues,this.props)}measureViewportBox(){return this.current?this.measureInstanceViewportBox(this.current,this.props):ze()}getStaticValue(t){return this.latestValues[t]}setStaticValue(t,e){this.latestValues[t]=e}update(t,e){(t.transformTemplate||this.props.transformTemplate)&&this.scheduleRender(),this.prevProps=this.props,this.props=t,this.prevPresenceContext=this.presenceContext,this.presenceContext=e;for(let i=0;ie.variantChildren.delete(t)}addValue(t,e){const i=this.values.get(t);e!==i&&(i&&this.removeValue(t),this.bindToMotionValue(t,e),this.values.set(t,e),this.latestValues[t]=e.get())}removeValue(t){this.values.delete(t);const e=this.valueSubscriptions.get(t);e&&(e(),this.valueSubscriptions.delete(t)),delete this.latestValues[t],this.removeValueFromRenderState(t,this.renderState)}hasValue(t){return this.values.has(t)}getValue(t,e){if(this.props.values&&this.props.values[t])return this.props.values[t];let i=this.values.get(t);return i===void 0&&e!==void 0&&(i=Vs(e===null?void 0:e,{owner:this}),this.addValue(t,i)),i}readValue(t,e){let i=this.latestValues[t]!==void 0||!this.current?this.latestValues[t]:this.getBaseTargetFromProps(this.props,t)??this.readValueFromInstance(this.current,t,this.options);return i!=null&&(typeof i=="string"&&(Q8(i)||t7(i))?i=parseFloat(i):!fA(i)&&ci.test(e)&&(i=$7(t,e)),this.setBaseTarget(t,Ae(i)?i.get():i)),Ae(i)?i.get():i}setBaseTarget(t,e){this.baseTarget[t]=e}getBaseTarget(t){const{initial:e}=this.props;let i;if(typeof e=="string"||typeof e=="object"){const o=o2(this.props,e,this.presenceContext?.custom);o&&(i=o[t])}if(e&&i!==void 0)return i;const a=this.getBaseTargetFromProps(this.props,t);return a!==void 0&&!Ae(a)?a:this.initialValues[t]!==void 0&&i===void 0?void 0:this.baseTarget[t]}on(t,e){return this.events[t]||(this.events[t]=new Ym),this.events[t].add(e)}notify(t,...e){this.events[t]&&this.events[t].notify(...e)}scheduleRenderMicrotask(){f2.render(this.render)}}class s9 extends i9{constructor(){super(...arguments),this.KeyframeResolver=q_}sortInstanceNodePosition(t,e){return t.compareDocumentPosition(e)&2?1:-1}getBaseTargetFromProps(t,e){const i=t.style;return i?i[e]:void 0}removeValueFromRenderState(t,{vars:e,style:i}){delete e[t],delete i[t]}handleChildMotionValue(){this.childSubscription&&(this.childSubscription(),delete this.childSubscription);const{children:t}=this.props;Ae(t)&&(this.childSubscription=t.on("change",e=>{this.current&&(this.current.textContent=`${e}`)}))}}class js{constructor(t){this.isMounted=!1,this.node=t}update(){}}function r9({top:n,left:t,right:e,bottom:i}){return{x:{min:t,max:e},y:{min:n,max:i}}}function yA({x:n,y:t}){return{top:t.min,right:n.max,bottom:t.max,left:n.min}}function vA(n,t){if(!t)return n;const e=t({x:n.left,y:n.top}),i=t({x:n.right,y:n.bottom});return{top:e.y,left:e.x,bottom:i.y,right:i.x}}function fd(n){return n===void 0||n===1}function cm({scale:n,scaleX:t,scaleY:e}){return!fd(n)||!fd(t)||!fd(e)}function pr(n){return cm(n)||a9(n)||n.z||n.rotate||n.rotateX||n.rotateY||n.skewX||n.skewY}function a9(n){return Py(n.x)||Py(n.y)}function Py(n){return n&&n!=="0%"}function sh(n,t,e){const i=n-e,a=t*i;return e+a}function Fy(n,t,e,i,a){return a!==void 0&&(n=sh(n,a,i)),sh(n,e,i)+t}function um(n,t=0,e=1,i,a){n.min=Fy(n.min,t,e,i,a),n.max=Fy(n.max,t,e,i,a)}function o9(n,{x:t,y:e}){um(n.x,t.translate,t.scale,t.originPoint),um(n.y,e.translate,e.scale,e.originPoint)}const Uy=.999999999999,ky=1.0000000000001;function bA(n,t,e,i=!1){const a=e.length;if(!a)return;t.x=t.y=1;let o,c;for(let u=0;uUy&&(t.x=1),t.yUy&&(t.y=1)}function Ti(n,t){n.min+=t,n.max+=t}function zy(n,t,e,i,a=.5){const o=ye(n.min,n.max,a);um(n,t,e,o,i)}function Vy(n,t){return typeof n=="string"?parseFloat(n)/100*(t.max-t.min):n}function Vu(n,t,e){const i=e??n;zy(n.x,Vy(t.x,i.x),t.scaleX,t.scale,t.originX),zy(n.y,Vy(t.y,i.y),t.scaleY,t.scale,t.originY)}function l9(n,t){return r9(vA(n.getBoundingClientRect(),t))}function xA(n,t,e){const i=l9(n,e),{scroll:a}=t;return a&&(Ti(i.x,a.offset.x),Ti(i.y,a.offset.y)),i}const TA={x:"translateX",y:"translateY",z:"translateZ",transformPerspective:"perspective"},CA=Ka.length;function SA(n,t,e){let i="",a=!0;for(let c=0;c{if(!t.target)return n;if(typeof n=="string")if(_t.test(n))n=parseFloat(n);else return n;const e=Gy(n,t.target.x),i=Gy(n,t.target.y);return`${e}% ${i}%`}},_A={correct:(n,{treeScale:t,projectionDelta:e})=>{const i=n,a=ci.parse(n);if(a.length>5)return i;const o=ci.createTransformer(n),c=typeof a[0]!="number"?1:0,u=e.x.scale*t.x,f=e.y.scale*t.y;a[0+c]/=u,a[1+c]/=f;const d=ye(u,f,.5);return typeof a[2+c]=="number"&&(a[2+c]/=d),typeof a[3+c]=="number"&&(a[3+c]/=d),o(a)}},hm={borderRadius:{...el,applyTo:[...u2]},borderTopLeftRadius:el,borderTopRightRadius:el,borderBottomLeftRadius:el,borderBottomRightRadius:el,boxShadow:_A};function u9(n,{layout:t,layoutId:e}){return Qa.has(n)||n.startsWith("origin")||(t||e!==void 0)&&(!!hm[n]||n==="opacity")}function y2(n,t,e){const i=n.style,a=t?.style,o={};if(!i)return o;for(const c in i)(Ae(i[c])||a&&Ae(a[c])||u9(c,n)||e?.getValue(c)?.liveStyle!==void 0)&&(o[c]=i[c]);return o}function AA(n){return window.getComputedStyle(n)}class h9 extends s9{constructor(){super(...arguments),this.type="html",this.renderInstance=c9}mount(t){xh(!!t.style),super.mount(t)}readValueFromInstance(t,e){if(Qa.has(e))return this.projection?.isProjecting?Z0(e):qS(t,e);{const i=AA(t),a=(g7(e)?i.getPropertyValue(e):i[e])||0;return typeof a=="string"?a.trim():a}}measureInstanceViewportBox(t,{transformPagePoint:e}){return l9(t,e)}build(t,e,i){g2(t,e,i.transformTemplate)}scrapeMotionValuesFromProps(t,e,i){return y2(t,e,i)}}function EA(n,t){return n in t}class wA extends i9{constructor(){super(...arguments),this.type="object"}readValueFromInstance(t,e){if(EA(e,t)){const i=t[e];if(typeof i=="string"||typeof i=="number")return i}}getBaseTargetFromProps(){}removeValueFromRenderState(t,e){delete e.output[t]}measureInstanceViewportBox(){return ze()}build(t,e){Object.assign(t.output,e)}renderInstance(t,{output:e}){Object.assign(t,e)}sortInstanceNodePosition(){return 0}}const MA={offset:"stroke-dashoffset",array:"stroke-dasharray"},RA={offset:"strokeDashoffset",array:"strokeDasharray"};function LA(n,t,e=1,i=0,a=!0){n.pathLength=1;const o=a?MA:RA;n[o.offset]=`${-i}`,n[o.array]=`${t} ${e}`}const NA=["offsetDistance","offsetPath","offsetRotate","offsetAnchor"];function f9(n,{attrX:t,attrY:e,attrScale:i,pathLength:a,pathSpacing:o=1,pathOffset:c=0,...u},f,d,m){if(g2(n,u,d),f){n.style.viewBox&&(n.attrs.viewBox=n.style.viewBox);return}n.attrs=n.style,n.style={};const{attrs:g,style:v}=n;g.transform&&(v.transform=g.transform,delete g.transform),(v.transform||g.transformOrigin)&&(v.transformOrigin=g.transformOrigin??"50% 50%",delete g.transformOrigin),v.transform&&(v.transformBox=m?.transformBox??"fill-box",delete g.transformBox);for(const x of NA)g[x]!==void 0&&(v[x]=g[x],delete g[x]);t!==void 0&&(g.x=t),e!==void 0&&(g.y=e),i!==void 0&&(g.scale=i),a!==void 0&&LA(g,a,o,c,!1)}const d9=new Set(["baseFrequency","diffuseConstant","kernelMatrix","kernelUnitLength","keySplines","keyTimes","limitingConeAngle","markerHeight","markerWidth","numOctaves","targetX","targetY","surfaceScale","specularConstant","specularExponent","stdDeviation","tableValues","viewBox","gradientTransform","pathLength","startOffset","textLength","lengthAdjust"]),m9=n=>typeof n=="string"&&n.toLowerCase()==="svg";function OA(n,t,e,i){c9(n,t,void 0,i);for(const a in t.attrs)n.setAttribute(d9.has(a)?a:l2(a),t.attrs[a])}function p9(n,t,e){const i=y2(n,t,e);for(const a in n)if(Ae(n[a])||Ae(t[a])){const o=Ka.indexOf(a)!==-1?"attr"+a.charAt(0).toUpperCase()+a.substring(1):a;i[o]=n[a]}return i}class g9 extends s9{constructor(){super(...arguments),this.type="svg",this.isSVGTag=!1,this.measureInstanceViewportBox=ze}getBaseTargetFromProps(t,e){return t[e]}readValueFromInstance(t,e){if(Qa.has(e)){const i=Y7(e);return i&&i.default||0}return e=d9.has(e)?e:l2(e),t.getAttribute(e)}scrapeMotionValuesFromProps(t,e,i){return p9(t,e,i)}build(t,e,i){f9(t,e,this.isSVGTag,i.transformTemplate,i.style)}renderInstance(t,e,i,a){OA(t,e,i,a)}mount(t){this.isSVGTag=m9(t.tagName),super.mount(t)}}const IA=p2.length;function y9(n){if(!n)return;if(!n.isControllingVariants){const e=n.parent?y9(n.parent)||{}:{};return n.props.initial!==void 0&&(e.initial=n.props.initial),e}const t={};for(let e=0;ePromise.all(t.map(({animation:e,options:i})=>D_(n,e,i)))}function FA(n){let t=PA(n),e=jy(),i=!0,a=!1;const o=d=>(m,g)=>{const v=Rr(n,g,d==="exit"?n.presenceContext?.custom:void 0);if(v){const{transition:x,transitionEnd:b,...C}=v;m={...m,...C,...b}}return m};function c(d){t=d(n)}function u(d){const{props:m}=n,g=y9(n.parent)||{},v=[],x=new Set;let b={},C=1/0;for(let S=0;SC&&w,k=!1;const U=Array.isArray(P)?P:[P];let G=U.reduce(o(L),{});F===!1&&(G={});const{prevResolvedValues:J={}}=R,tt={...J,...G},K=N=>{I=!0,x.has(N)&&(k=!0,x.delete(N)),R.needsAnimating[N]=!0;const H=n.getValue(N);H&&(H.liveStyle=!1)};for(const N in tt){const H=G[N],V=J[N];if(b.hasOwnProperty(N))continue;let M=!1;nm(H)&&nm(V)?M=!v9(H,V)||O:M=H!==V,M?H!=null?K(N):x.add(N):H!==void 0&&x.has(N)?K(N):R.protectedKeys[N]=!0}R.prevProp=P,R.prevResolvedValues=G,R.isActive&&(b={...b,...G}),(i||a)&&n.blockInitialAnimation&&(I=!1);const A=E&&O;I&&(!A||k)&&v.push(...U.map(N=>{const H={type:L};if(typeof N=="string"&&(i||a)&&!A&&n.manuallyAnimateOnMount&&n.parent){const{parent:V}=n,M=Rr(V,N);if(V.enteringChildren&&M){const{delayChildren:z}=M.transition||{};H.delay=k7(V.enteringChildren,n,z)}}return{animation:N,options:H}}))}if(x.size){const S={};if(typeof m.initial!="boolean"){const L=Rr(n,Array.isArray(m.initial)?m.initial[0]:m.initial);L&&L.transition&&(S.transition=L.transition)}x.forEach(L=>{const R=n.getBaseTarget(L),P=n.getValue(L);P&&(P.liveStyle=!0),S[L]=R??null}),v.push({animation:S})}let T=!!v.length;return i&&(m.initial===!1||m.initial===m.animate)&&!n.manuallyAnimateOnMount&&(T=!1),i=!1,a=!1,T?t(v):Promise.resolve()}function f(d,m){if(e[d].isActive===m)return Promise.resolve();n.variantChildren?.forEach(v=>v.animationState?.setActive(d,m)),e[d].isActive=m;const g=u(d);for(const v in e)e[v].protectedKeys={};return g}return{animateChanges:u,setActive:f,setAnimateFunction:c,getState:()=>e,reset:()=>{e=jy(),a=!0}}}function UA(n,t){return typeof t=="string"?t!==n:Array.isArray(t)?!v9(t,n):!1}function ur(n=!1){return{isActive:n,protectedKeys:{},needsAnimating:{},prevResolvedValues:{}}}function jy(){return{animate:ur(!0),whileInView:ur(),whileHover:ur(),whileTap:ur(),whileDrag:ur(),whileFocus:ur(),exit:ur()}}function fm(n,t){n.min=t.min,n.max=t.max}function ii(n,t){fm(n.x,t.x),fm(n.y,t.y)}function Hy(n,t){n.translate=t.translate,n.scale=t.scale,n.originPoint=t.originPoint,n.origin=t.origin}const b9=1e-4,kA=1-b9,zA=1+b9,x9=.01,VA=0-x9,GA=0+x9;function mn(n){return n.max-n.min}function jA(n,t,e){return Math.abs(n-t)<=e}function qy(n,t,e,i=.5){n.origin=i,n.originPoint=ye(t.min,t.max,n.origin),n.scale=mn(e)/mn(t),n.translate=ye(e.min,e.max,n.origin)-n.originPoint,(n.scale>=kA&&n.scale<=zA||isNaN(n.scale))&&(n.scale=1),(n.translate>=VA&&n.translate<=GA||isNaN(n.translate))&&(n.translate=0)}function _l(n,t,e,i){qy(n.x,t.x,e.x,i?i.originX:void 0),qy(n.y,t.y,e.y,i?i.originY:void 0)}function Xy(n,t,e,i=0){const a=i?ye(e.min,e.max,i):e.min;n.min=a+t.min,n.max=n.min+mn(t)}function HA(n,t,e,i){Xy(n.x,t.x,e.x,i?.x),Xy(n.y,t.y,e.y,i?.y)}function Yy(n,t,e,i=0){const a=i?ye(e.min,e.max,i):e.min;n.min=t.min-a,n.max=n.min+mn(t)}function rh(n,t,e,i){Yy(n.x,t.x,e.x,i?.x),Yy(n.y,t.y,e.y,i?.y)}function $y(n,t,e,i,a){return n-=t,n=sh(n,1/e,i),a!==void 0&&(n=sh(n,1/a,i)),n}function qA(n,t=0,e=1,i=.5,a,o=n,c=n){if(Si.test(t)&&(t=parseFloat(t),t=ye(c.min,c.max,t/100)-c.min),typeof t!="number")return;let u=ye(o.min,o.max,i);n===o&&(u-=t),n.min=$y(n.min,t,e,u,a),n.max=$y(n.max,t,e,u,a)}function Wy(n,t,[e,i,a],o,c){qA(n,t[e],t[i],t[a],t.scale,o,c)}const XA=["x","scaleX","originX"],YA=["y","scaleY","originY"];function Zy(n,t,e,i){Wy(n.x,t,XA,e?e.x:void 0,i?i.x:void 0),Wy(n.y,t,YA,e?e.y:void 0,i?i.y:void 0)}function Ky(n){return n.translate===0&&n.scale===1}function T9(n){return Ky(n.x)&&Ky(n.y)}function Qy(n,t){return n.min===t.min&&n.max===t.max}function $A(n,t){return Qy(n.x,t.x)&&Qy(n.y,t.y)}function Jy(n,t){return Math.round(n.min)===Math.round(t.min)&&Math.round(n.max)===Math.round(t.max)}function C9(n,t){return Jy(n.x,t.x)&&Jy(n.y,t.y)}function t5(n){return mn(n.x)/mn(n.y)}function e5(n,t){return n.translate===t.translate&&n.scale===t.scale&&n.originPoint===t.originPoint}function xi(n){return[n("x"),n("y")]}function WA(n,t,e){let i="";const a=n.x.translate/t.x,o=n.y.translate/t.y,c=e?.z||0;if((a||o||c)&&(i=`translate3d(${a}px, ${o}px, ${c}px) `),(t.x!==1||t.y!==1)&&(i+=`scale(${1/t.x}, ${1/t.y}) `),e){const{transformPerspective:d,rotate:m,pathRotation:g,rotateX:v,rotateY:x,skewX:b,skewY:C}=e;d&&(i=`perspective(${d}px) ${i}`),m&&(i+=`rotate(${m}deg) `),g&&(i+=`rotate(${g}deg) `),v&&(i+=`rotateX(${v}deg) `),x&&(i+=`rotateY(${x}deg) `),b&&(i+=`skewX(${b}deg) `),C&&(i+=`skewY(${C}deg) `)}const u=n.x.scale*t.x,f=n.y.scale*t.y;return(u!==1||f!==1)&&(i+=`scale(${u}, ${f})`),i||"none"}const ZA=u2.length,n5=n=>typeof n=="string"?parseFloat(n):n,i5=n=>typeof n=="number"||_t.test(n);function KA(n,t,e,i,a,o){a?(n.opacity=ye(0,e.opacity??1,QA(i)),n.opacityExit=ye(t.opacity??1,0,JA(i))):o&&(n.opacity=ye(t.opacity??1,e.opacity??1,i));for(let c=0;cit?1:e(Xa(n,t,i))}function _9(n,t,e){const i=Ae(n)?n:Vs(n);return i.start(a2("",i,t,e)),i.animation}function Fl(n,t,e,i={passive:!0}){return n.addEventListener(t,e,i),()=>n.removeEventListener(t,e,i)}const tE=(n,t)=>n.depth-t.depth;class eE{constructor(){this.children=[],this.isDirty=!1}add(t){Xm(this.children,t),this.isDirty=!0}remove(t){qa(this.children,t),this.isDirty=!0}forEach(t){this.isDirty&&this.children.sort(tE),this.isDirty=!1,this.children.forEach(t)}}function nE(n,t){const e=dn.now(),i=({timestamp:a})=>{const o=a-e;o>=t&&(wi(i),n(o-t))};return re.setup(i,!0),()=>wi(i)}function Gu(n){return Ae(n)?n.get():n}class iE{constructor(){this.members=[]}add(t){Xm(this.members,t);for(let e=this.members.length-1;e>=0;e--){const i=this.members[e];if(i===t||i===this.lead||i===this.prevLead)continue;const a=i.instance;(!a||a.isConnected===!1)&&!i.snapshot&&(qa(this.members,i),i.unmount())}t.scheduleRender()}remove(t){if(qa(this.members,t),t===this.prevLead&&(this.prevLead=void 0),t===this.lead){const e=this.members[this.members.length-1];e&&this.promote(e)}}relegate(t){for(let e=this.members.indexOf(t)-1;e>=0;e--){const i=this.members[e];if(i.isPresent!==!1&&i.instance?.isConnected!==!1)return this.promote(i),!0}return!1}promote(t,e){const i=this.lead;if(t!==i&&(this.prevLead=i,this.lead=t,t.show(),i)){i.updateSnapshot(),t.scheduleRender();const{layoutDependency:a}=i.options,{layoutDependency:o}=t.options;(a===void 0||a!==o)&&(t.resumeFrom=i,e&&(i.preserveOpacity=!0),i.snapshot&&(t.snapshot=i.snapshot,t.snapshot.latestValues=i.animationValues||i.latestValues),t.root?.isUpdating&&(t.isLayoutDirty=!0)),t.options.crossfade===!1&&i.hide()}}exitAnimationComplete(){this.members.forEach(t=>{t.options.onExitComplete?.(),t.resumingFrom?.options.onExitComplete?.()})}scheduleRender(){this.members.forEach(t=>t.instance&&t.scheduleRender(!1))}removeLeadSnapshot(){this.lead?.snapshot&&(this.lead.snapshot=void 0)}}const ju={hasAnimatedSinceResize:!0,hasEverUpdated:!1},dd=["","X","Y","Z"],sE=1e3;let rE=0;function md(n,t,e,i){const{latestValues:a}=t;a[n]&&(e[n]=a[n],t.setStaticValue(n,0),i&&(i[n]=0))}function A9(n){if(n.hasCheckedOptimisedAppear=!0,n.root===n)return;const{visualElement:t}=n.options;if(!t)return;const e=H7(t);if(window.MotionHasOptimisedAnimation(e,"transform")){const{layout:a,layoutId:o}=n.options;window.MotionCancelOptimisedAnimation(e,"transform",re,!(a||o))}const{parent:i}=n;i&&!i.hasCheckedOptimisedAppear&&A9(i)}function E9({attachResizeListener:n,defaultParent:t,measureScroll:e,checkIsScrollRoot:i,resetTransform:a}){return class{constructor(c={},u=t?.()){this.id=rE++,this.animationId=0,this.animationCommitId=0,this.children=new Set,this.options={},this.isTreeAnimating=!1,this.isAnimationBlocked=!1,this.isLayoutDirty=!1,this.isProjectionDirty=!1,this.isSharedProjectionDirty=!1,this.isTransformDirty=!1,this.updateManuallyBlocked=!1,this.updateBlockedByResize=!1,this.isUpdating=!1,this.isSVG=!1,this.needsReset=!1,this.shouldResetTransform=!1,this.hasCheckedOptimisedAppear=!1,this.treeScale={x:1,y:1},this.eventHandlers=new Map,this.hasTreeAnimated=!1,this.layoutVersion=0,this.updateScheduled=!1,this.scheduleUpdate=()=>this.update(),this.projectionUpdateScheduled=!1,this.checkUpdateFailed=()=>{this.isUpdating&&(this.isUpdating=!1,this.clearAllSnapshots())},this.updateProjection=()=>{this.projectionUpdateScheduled=!1,this.nodes.forEach(lE),this.nodes.forEach(mE),this.nodes.forEach(pE),this.nodes.forEach(cE)},this.resolvedRelativeTargetAt=0,this.linkedParentVersion=0,this.hasProjected=!1,this.isVisible=!0,this.animationProgress=0,this.sharedNodes=new Map,this.latestValues=c,this.root=u?u.root||u:this,this.path=u?[...u.path,u]:[],this.parent=u,this.depth=u?u.depth+1:0;for(let f=0;fthis.root.updateBlockedByResize=!1;re.read(()=>{g=window.innerWidth}),n(c,()=>{const x=window.innerWidth;x!==g&&(g=x,this.root.updateBlockedByResize=!0,m&&m(),m=nE(v,250),ju.hasAnimatedSinceResize&&(ju.hasAnimatedSinceResize=!1,this.nodes.forEach(o5)))})}u&&this.root.registerSharedNode(u,this),this.options.animate!==!1&&d&&(u||f)&&this.addEventListener("didUpdate",({delta:m,hasLayoutChanged:g,hasRelativeLayoutChanged:v,layout:x})=>{if(this.isTreeAnimationBlocked()){this.target=void 0,this.relativeTarget=void 0;return}const b=this.options.transition||d.getDefaultTransition()||xE,{onLayoutAnimationStart:C,onLayoutAnimationComplete:T}=d.getProps(),S=!this.targetLayout||!C9(this.targetLayout,x),L=!g&&v;if(this.options.layoutRoot||this.resumeFrom||L||g&&(S||!this.currentAnimation)){this.resumeFrom&&(this.resumingFrom=this.resumeFrom,this.resumingFrom.resumingFrom=void 0);const R={...r2(b,"layout"),onPlay:C,onComplete:T};(d.shouldReduceMotion||this.options.layoutRoot)&&(R.delay=0,R.type=!1),this.startAnimation(R),this.setAnimationOrigin(m,L,R.path)}else g||o5(this),this.isLead()&&this.options.onExitComplete&&this.options.onExitComplete();this.targetLayout=x})}unmount(){this.options.layoutId&&this.willUpdate(),this.root.nodes.remove(this);const c=this.getStack();c&&c.remove(this),this.parent&&this.parent.children.delete(this),this.instance=void 0,this.eventHandlers.clear(),wi(this.updateProjection)}blockUpdate(){this.updateManuallyBlocked=!0}unblockUpdate(){this.updateManuallyBlocked=!1}isUpdateBlocked(){return this.updateManuallyBlocked||this.updateBlockedByResize}isTreeAnimationBlocked(){return this.isAnimationBlocked||this.parent&&this.parent.isTreeAnimationBlocked()||!1}startUpdate(){this.isUpdateBlocked()||(this.isUpdating=!0,this.nodes&&this.nodes.forEach(gE),this.animationId++)}getTransformTemplate(){const{visualElement:c}=this.options;return c&&c.getProps().transformTemplate}willUpdate(c=!0){if(this.root.hasTreeAnimated=!0,this.root.isUpdateBlocked()){this.options.onExitComplete&&this.options.onExitComplete();return}if(window.MotionCancelOptimisedAnimation&&!this.hasCheckedOptimisedAppear&&A9(this),!this.root.isUpdating&&this.root.startUpdate(),this.isLayoutDirty)return;this.isLayoutDirty=!0;for(let m=0;m{this.isLayoutDirty?this.root.didUpdate():this.root.checkUpdateFailed()})}updateSnapshot(){this.snapshot||!this.instance||(this.snapshot=this.measure(),this.snapshot&&!mn(this.snapshot.measuredBox.x)&&!mn(this.snapshot.measuredBox.y)&&(this.snapshot=void 0))}updateLayout(){if(!this.instance||(this.updateScroll(),!(this.options.alwaysMeasureLayout&&this.isLead())&&!this.isLayoutDirty))return;if(this.resumeFrom&&!this.resumeFrom.instance)for(let f=0;f{const E=F/1e3,O=w?.(E);O?(v.x.translate=O.x,v.x.scale=ye(c.x.scale,1,E),v.x.origin=c.x.origin,v.x.originPoint=c.x.originPoint,v.y.translate=O.y,v.y.scale=ye(c.y.scale,1,E),v.y.origin=c.y.origin,v.y.originPoint=c.y.originPoint):(l5(v.x,c.x,E),l5(v.y,c.y,E)),this.setTargetDelta(v),this.relativeTarget&&this.relativeTargetOrigin&&this.layout&&this.relativeParent&&this.relativeParent.layout&&(rh(x,this.layout.layoutBox,this.relativeParent.layout.layoutBox,this.options.layoutAnchor||void 0),vE(this.relativeTarget,this.relativeTargetOrigin,x,E),P&&$A(this.relativeTarget,P)&&(this.isProjectionDirty=!1),P||(P=ze()),ii(P,this.relativeTarget)),T&&(this.animationValues=g,KA(g,m,this.latestValues,E,R,L)),O&&O.rotate!==void 0&&(this.animationValues||(this.animationValues=g),this.animationValues.pathRotation=O.rotate),this.root.scheduleUpdateProjection(),this.scheduleRender(),this.animationProgress=E},this.mixTargetDelta(this.options.layoutRoot?1e3:0)}startAnimation(c){this.notifyListeners("animationStart"),this.currentAnimation?.stop(),this.resumingFrom?.currentAnimation?.stop(),this.pendingAnimation&&(wi(this.pendingAnimation),this.pendingAnimation=void 0),this.pendingAnimation=re.update(()=>{ju.hasAnimatedSinceResize=!0,this.motionValue||(this.motionValue=Vs(0)),this.motionValue.jump(0,!1),this.currentAnimation=_9(this.motionValue,[0,1e3],{...c,velocity:0,isSync:!0,onUpdate:u=>{this.mixTargetDelta(u),c.onUpdate&&c.onUpdate(u)},onComplete:()=>{c.onComplete&&c.onComplete(),this.completeAnimation()}}),this.resumingFrom&&(this.resumingFrom.currentAnimation=this.currentAnimation),this.pendingAnimation=void 0})}completeAnimation(){this.resumingFrom&&(this.resumingFrom.currentAnimation=void 0,this.resumingFrom.preserveOpacity=void 0);const c=this.getStack();c&&c.exitAnimationComplete(),this.resumingFrom=this.currentAnimation=this.animationValues=void 0,this.notifyListeners("animationComplete")}finishAnimation(){this.currentAnimation&&(this.mixTargetDelta&&this.mixTargetDelta(sE),this.currentAnimation.stop()),this.completeAnimation()}applyTransformsToTarget(){const c=this.getLead();let{targetWithTransforms:u,target:f,layout:d,latestValues:m}=c;if(!(!u||!f||!d)){if(this!==c&&this.layout&&d&&w9(this.options.animationType,this.layout.layoutBox,d.layoutBox)){f=this.target||ze();const g=mn(this.layout.layoutBox.x);f.x.min=c.target.x.min,f.x.max=f.x.min+g;const v=mn(this.layout.layoutBox.y);f.y.min=c.target.y.min,f.y.max=f.y.min+v}ii(u,f),Vu(u,m),_l(this.projectionDeltaWithTransform,this.layoutCorrected,u,m)}}registerSharedNode(c,u){this.sharedNodes.has(c)||this.sharedNodes.set(c,new iE),this.sharedNodes.get(c).add(u);const d=u.options.initialPromotionConfig;u.promote({transition:d?d.transition:void 0,preserveFollowOpacity:d&&d.shouldPreserveFollowOpacity?d.shouldPreserveFollowOpacity(u):void 0})}isLead(){const c=this.getStack();return c?c.lead===this:!0}getLead(){const{layoutId:c}=this.options;return c?this.getStack()?.lead||this:this}getPrevLead(){const{layoutId:c}=this.options;return c?this.getStack()?.prevLead:void 0}getStack(){const{layoutId:c}=this.options;if(c)return this.root.sharedNodes.get(c)}promote({needsReset:c,transition:u,preserveFollowOpacity:f}={}){const d=this.getStack();d&&d.promote(this,f),c&&(this.projectionDelta=void 0,this.needsReset=!0),u&&this.setOptions({transition:u})}relegate(){const c=this.getStack();return c?c.relegate(this):!1}resetSkewAndRotation(){const{visualElement:c}=this.options;if(!c)return;let u=!1;const{latestValues:f}=c;if((f.z||f.rotate||f.rotateX||f.rotateY||f.rotateZ||f.skewX||f.skewY)&&(u=!0),!u)return;const d={};f.z&&md("z",c,d,this.animationValues);for(let m=0;mc.currentAnimation?.stop()),this.root.nodes.forEach(r5),this.root.sharedNodes.clear()}}}function aE(n){n.updateLayout()}function oE(n){const t=n.resumeFrom?.snapshot||n.snapshot;if(n.isLead()&&n.layout&&t&&n.hasListeners("didUpdate")){const{layoutBox:e,measuredBox:i}=n.layout,{animationType:a}=n.options,o=t.source!==n.layout.source;if(a==="size")xi(m=>{const g=o?t.measuredBox[m]:t.layoutBox[m],v=mn(g);g.min=e[m].min,g.max=g.min+v});else if(a==="x"||a==="y"){const m=a==="x"?"y":"x";fm(o?t.measuredBox[m]:t.layoutBox[m],e[m])}else w9(a,t.layoutBox,e)&&xi(m=>{const g=o?t.measuredBox[m]:t.layoutBox[m],v=mn(e[m]);g.max=g.min+v,n.relativeTarget&&!n.currentAnimation&&(n.isProjectionDirty=!0,n.relativeTarget[m].max=n.relativeTarget[m].min+v)});const c=Ua();_l(c,e,t.layoutBox);const u=Ua();o?_l(u,n.applyTransform(i,!0),t.measuredBox):_l(u,e,t.layoutBox);const f=!T9(c);let d=!1;if(!n.resumeFrom){const m=n.getClosestProjectingParent();if(m&&!m.resumeFrom){const{snapshot:g,layout:v}=m;if(g&&v){const x=n.options.layoutAnchor||void 0,b=ze();rh(b,t.layoutBox,g.layoutBox,x);const C=ze();rh(C,e,v.layoutBox,x),C9(b,C)||(d=!0),m.options.layoutRoot&&(n.relativeTarget=C,n.relativeTargetOrigin=b,n.relativeParent=m)}}}n.notifyListeners("didUpdate",{layout:e,snapshot:t,delta:u,layoutDelta:c,hasLayoutChanged:f,hasRelativeLayoutChanged:d})}else if(n.isLead()){const{onExitComplete:e}=n.options;e&&e()}n.options.transition=void 0}function lE(n){n.parent&&(n.isProjecting()||(n.isProjectionDirty=n.parent.isProjectionDirty),n.isSharedProjectionDirty||(n.isSharedProjectionDirty=!!(n.isProjectionDirty||n.parent.isProjectionDirty||n.parent.isSharedProjectionDirty)),n.isTransformDirty||(n.isTransformDirty=n.parent.isTransformDirty))}function cE(n){n.isProjectionDirty=n.isSharedProjectionDirty=n.isTransformDirty=!1}function uE(n){n.clearSnapshot()}function r5(n){n.clearMeasurements()}function hE(n){n.isLayoutDirty=!0,n.updateLayout()}function a5(n){n.isLayoutDirty=!1}function fE(n){n.isAnimationBlocked&&n.layout&&!n.isLayoutDirty&&(n.snapshot=n.layout,n.isLayoutDirty=!0)}function dE(n){const{visualElement:t}=n.options;t&&t.getProps().onBeforeLayoutMeasure&&t.notify("BeforeLayoutMeasure"),n.resetTransform()}function o5(n){n.finishAnimation(),n.targetDelta=n.relativeTarget=n.target=void 0,n.isProjectionDirty=!0}function mE(n){n.resolveTargetDelta()}function pE(n){n.calcProjection()}function gE(n){n.resetSkewAndRotation()}function yE(n){n.removeLeadSnapshot()}function l5(n,t,e){n.translate=ye(t.translate,0,e),n.scale=ye(t.scale,1,e),n.origin=t.origin,n.originPoint=t.originPoint}function c5(n,t,e,i){n.min=ye(t.min,e.min,i),n.max=ye(t.max,e.max,i)}function vE(n,t,e,i){c5(n.x,t.x,e.x,i),c5(n.y,t.y,e.y,i)}function bE(n){return n.animationValues&&n.animationValues.opacityExit!==void 0}const xE={duration:.45,ease:[.4,0,.1,1]},u5=n=>typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().includes(n),h5=u5("applewebkit/")&&!u5("chrome/")?Math.round:Zn;function f5(n){n.min=h5(n.min),n.max=h5(n.max)}function TE(n){f5(n.x),f5(n.y)}function w9(n,t,e){return n==="position"||n==="preserve-aspect"&&!jA(t5(t),t5(e),.2)}function CE(n){return n!==n.root&&n.scroll?.wasRoot}const SE=E9({attachResizeListener:(n,t)=>Fl(n,"resize",t),measureScroll:()=>({x:document.documentElement.scrollLeft||document.body?.scrollLeft||0,y:document.documentElement.scrollTop||document.body?.scrollTop||0}),checkIsScrollRoot:()=>!0}),_E=n=>!n.isLayoutDirty&&n.willUpdate(!1);function d5(){const n=new Set,t=new WeakMap,e=()=>n.forEach(_E);return{add:i=>{n.add(i),t.set(i,i.addEventListener("willUpdate",e))},remove:i=>{n.delete(i);const a=t.get(i);a&&(a(),t.delete(i)),e()},dirty:e}}const pd={current:void 0},M9=E9({measureScroll:n=>({x:n.scrollLeft,y:n.scrollTop}),defaultParent:()=>{if(!pd.current){const n=new SE({});n.mount(window),n.setOptions({layoutScroll:!0}),pd.current=n}return pd.current},resetTransform:(n,t)=>{n.style.transform=t!==void 0?t:"none"},checkIsScrollRoot:n=>window.getComputedStyle(n).position==="fixed"}),Ja=q.createContext({transformPagePoint:n=>n,isStatic:!1,reducedMotion:"never"});function m5(n,t){if(typeof n=="function")return n(t);n!=null&&(n.current=t)}function AE(...n){return t=>{let e=!1;const i=n.map(a=>{const o=m5(a,t);return!e&&typeof o=="function"&&(e=!0),o});if(e)return()=>{for(let a=0;a{const{width:v,height:x,top:b,left:C,right:T,bottom:S,direction:L}=f.current;if(t||o===!1||!u.current||!v||!x)return;const R=L==="rtl",P=e==="left"?R?`right: ${T}`:`left: ${C}`:R?`left: ${C}`:`right: ${T}`,w=i==="bottom"?`bottom: ${S}`:`top: ${b}`;u.current.dataset.motionPopId=c;const F=document.createElement("style");d&&(F.nonce=d);const E=a??document.head;return E.appendChild(F),F.sheet&&F.sheet.insertRule(` + [data-motion-pop-id="${c}"] { + position: absolute !important; + width: ${v}px !important; + height: ${x}px !important; + ${P}px !important; + ${w}px !important; + } + `),()=>{u.current?.removeAttribute("data-motion-pop-id"),E.contains(F)&&E.removeChild(F)}},[t]),Y.jsx(wE,{isPresent:t,childRef:u,sizeRef:f,pop:o,children:o===!1?n:q.cloneElement(n,{ref:g})})}const RE=({children:n,initial:t,isPresent:e,onExitComplete:i,custom:a,presenceAffectsLayout:o,mode:c,anchorX:u,anchorY:f,root:d})=>{const m=Lr(LE),g=q.useId(),v=q.useRef(e),x=q.useRef(i);jl(()=>{v.current=e,x.current=i});let b=!0,C=q.useMemo(()=>(b=!1,{id:g,initial:t,isPresent:e,custom:a,onExitComplete:T=>{m.set(T,!0);for(const S of m.values())if(!S)return;i&&i()},register:T=>(m.set(T,!1),()=>{m.delete(T),!v.current&&!m.size&&x.current?.()})}),[e,m,i]);return o&&b&&(C={...C}),q.useMemo(()=>{m.forEach((T,S)=>m.set(S,!1))},[e]),q.useEffect(()=>{!e&&!m.size&&i&&i()},[e]),n=Y.jsx(ME,{pop:c==="popLayout",isPresent:e,anchorX:u,anchorY:f,root:d,children:n}),Y.jsx(bh.Provider,{value:C,children:n})};function LE(){return new Map}function R9(n=!0){const t=q.useContext(bh);if(t===null)return[!0,null];const{isPresent:e,onExitComplete:i,register:a}=t,o=q.useId();q.useEffect(()=>{if(n)return a(o)},[n]);const c=q.useCallback(()=>n&&i&&i(o),[o,i,n]);return!e&&i?[!1,c]:[!0]}const du=n=>n.key||"";function p5(n){const t=[];return q.Children.forEach(n,e=>{q.isValidElement(e)&&t.push(e)}),t}const NE=({children:n,custom:t,initial:e=!0,onExitComplete:i,presenceAffectsLayout:a=!0,mode:o="sync",propagate:c=!1,anchorX:u="left",anchorY:f="top",root:d})=>{const[m,g]=R9(c),v=q.useMemo(()=>p5(n),[n]),x=c&&!m?[]:v.map(du),b=q.useRef(!0),C=q.useRef(v),T=Lr(()=>new Map),S=q.useRef(new Set),[L,R]=q.useState(v),[P,w]=q.useState(v);jl(()=>{b.current=!1,C.current=v;for(let O=0;O{const I=du(O),k=c&&!m?!1:v===P||x.includes(I),U=()=>{if(S.current.has(I))return;if(T.has(I))S.current.add(I),T.set(I,!0);else return;let G=!0;T.forEach(J=>{J||(G=!1)}),G&&(E?.(),w(C.current),c&&g?.(),i&&i())};return Y.jsx(RE,{isPresent:k,initial:!b.current||e?void 0:!1,custom:t,presenceAffectsLayout:a,mode:o,root:d,onExitComplete:k?void 0:U,anchorX:u,anchorY:f,children:O},I)})})},OE=q.createContext(null);function IE(){const n=q.useRef(!1);return jl(()=>(n.current=!0,()=>{n.current=!1}),[]),n}function DE(){const n=IE(),[t,e]=q.useState(0),i=q.useCallback(()=>{n.current&&e(t+1)},[t]);return[q.useCallback(()=>re.postRender(i),[i]),t]}const L9=n=>n===!0,BE=n=>L9(n===!0)||n==="id",PE=({children:n,id:t,inherit:e=!0})=>{const i=q.useContext(Nl),a=q.useContext(OE),[o,c]=DE(),u=q.useRef(null),f=i.id||a;u.current===null&&(BE(e)&&f&&(t=t?f+"-"+t:f),u.current={id:t,group:L9(e)&&i.group||d5()});const d=q.useMemo(()=>({...u.current,forceRender:o}),[c]);return Y.jsx(Nl.Provider,{value:d,children:n})},N9=q.createContext({strict:!1}),g5={animation:["animate","variants","whileHover","whileTap","exit","whileInView","whileFocus","whileDrag"],exit:["exit"],drag:["drag","dragControls"],focus:["whileFocus"],hover:["whileHover","onHoverStart","onHoverEnd"],tap:["whileTap","onTap","onTapStart","onTapCancel"],pan:["onPan","onPanStart","onPanSessionStart","onPanEnd"],inView:["whileInView","onViewportEnter","onViewportLeave"],layout:["layout","layoutId"]};let y5=!1;function FE(){if(y5)return;const n={};for(const t in g5)n[t]={isEnabled:e=>g5[t].some(i=>!!e[i])};n9(n),y5=!0}function O9(){return FE(),gA()}function UE(n){const t=O9();for(const e in n)t[e]={...t[e],...n[e]};n9(t)}const kE=new Set(["animate","exit","variants","initial","style","values","variants","transition","transformTemplate","custom","inherit","onBeforeLayoutMeasure","onAnimationStart","onAnimationComplete","onUpdate","onDragStart","onDrag","onDragEnd","onMeasureDragConstraints","onDirectionLock","onDragTransitionEnd","_dragX","_dragY","onHoverStart","onHoverEnd","onViewportEnter","onViewportLeave","globalTapTarget","propagate","ignoreStrict","viewport"]);function ah(n){return n.startsWith("while")||n.startsWith("drag")&&n!=="draggable"||n.startsWith("layout")||n.startsWith("onTap")||n.startsWith("onPan")||n.startsWith("onLayout")||kE.has(n)}let I9=n=>!ah(n);function zE(n){typeof n=="function"&&(I9=t=>t.startsWith("on")?!ah(t):n(t))}try{zE(require("@emotion/is-prop-valid").default)}catch{}function VE(n,t,e){const i={};for(const a in n)a==="values"&&typeof n.values=="object"||Ae(n[a])||(I9(a)||e===!0&&ah(a)||!t&&!ah(a)||n.draggable&&a.startsWith("onDrag"))&&(i[a]=n[a]);return i}const Ah=q.createContext({});function GE(n,t){if(_h(n)){const{initial:e,animate:i}=n;return{initial:e===!1||Pl(e)?e:void 0,animate:Pl(i)?i:void 0}}return n.inherit!==!1?t:{}}function jE(n){const{initial:t,animate:e}=GE(n,q.useContext(Ah));return q.useMemo(()=>({initial:t,animate:e}),[v5(t),v5(e)])}function v5(n){return Array.isArray(n)?n.join(" "):n}const v2=()=>({style:{},transform:{},transformOrigin:{},vars:{}});function D9(n,t,e){for(const i in t)!Ae(t[i])&&!u9(i,e)&&(n[i]=t[i])}function HE({transformTemplate:n},t){return q.useMemo(()=>{const e=v2();return g2(e,t,n),Object.assign({},e.vars,e.style)},[t])}function qE(n,t){const e=n.style||{},i={};return D9(i,e,n),Object.assign(i,HE(n,t)),i}function XE(n,t){const e={},i=qE(n,t);return n.drag&&n.dragListener!==!1&&(e.draggable=!1,i.userSelect=i.WebkitUserSelect=i.WebkitTouchCallout="none",i.touchAction=n.drag===!0?"none":`pan-${n.drag==="x"?"y":"x"}`),n.tabIndex===void 0&&(n.onTap||n.onTapStart||n.whileTap)&&(e.tabIndex=0),e.style=i,e}const B9=()=>({...v2(),attrs:{}});function YE(n,t,e,i){const a=q.useMemo(()=>{const o=B9();return f9(o,t,m9(i),n.transformTemplate,n.style),{...o.attrs,style:{...o.style}}},[t]);if(n.style){const o={};D9(o,n.style,n),a.style={...o,...a.style}}return a}const $E=["animate","circle","defs","desc","ellipse","g","image","line","filter","marker","mask","metadata","path","pattern","polygon","polyline","rect","stop","switch","symbol","svg","text","tspan","use","view"];function b2(n){return typeof n!="string"||n.includes("-")?!1:!!($E.indexOf(n)>-1||/[A-Z]/u.test(n))}function WE(n,t,e,{latestValues:i},a,o=!1,c){const f=(c??b2(n)?YE:XE)(t,i,a,n),d=VE(t,typeof n=="string",o),m=n!==q.Fragment?{...d,...f,ref:e}:{},{children:g}=t,v=q.useMemo(()=>Ae(g)?g.get():g,[g]);return q.createElement(n,{...m,children:v})}function ZE({scrapeMotionValuesFromProps:n,createRenderState:t},e,i,a){return{latestValues:KE(e,i,a,n),renderState:t()}}function KE(n,t,e,i){const a={},o=i(n,{});for(const v in o)a[v]=Gu(o[v]);let{initial:c,animate:u}=n;const f=_h(n),d=t9(n);t&&d&&!f&&n.inherit!==!1&&(c===void 0&&(c=t.initial),u===void 0&&(u=t.animate));let m=e?e.initial===!1:!1;m=m||c===!1;const g=m?u:c;if(g&&typeof g!="boolean"&&!Sh(g)){const v=Array.isArray(g)?g:[g];for(let x=0;x(t,e)=>{const i=q.useContext(Ah),a=q.useContext(bh),o=()=>ZE(n,t,i,a);return e?o():Lr(o)},QE=P9({scrapeMotionValuesFromProps:y2,createRenderState:v2}),JE=P9({scrapeMotionValuesFromProps:p9,createRenderState:B9}),tw=Symbol.for("motionComponentSymbol");function ew(n,t,e){const i=q.useRef(e);q.useInsertionEffect(()=>{i.current=e});const a=q.useRef(null);return q.useCallback(o=>{o&&n.onMount?.(o),t&&(o?t.mount(o):t.unmount());const c=i.current;if(typeof c=="function")if(o){const u=c(o);typeof u=="function"&&(a.current=u)}else a.current?(a.current(),a.current=null):c(o);else c&&(c.current=o)},[t])}const F9=q.createContext({});function Na(n){return n&&typeof n=="object"&&Object.prototype.hasOwnProperty.call(n,"current")}function nw(n,t,e,i,a,o){const{visualElement:c}=q.useContext(Ah),u=q.useContext(N9),f=q.useContext(bh),d=q.useContext(Ja),m=d.reducedMotion,g=d.skipAnimations,v=q.useRef(null),x=q.useRef(!1);i=i||u.renderer,!v.current&&i&&(v.current=i(n,{visualState:t,parent:c,props:e,presenceContext:f,blockInitialAnimation:f?f.initial===!1:!1,reducedMotionConfig:m,skipAnimations:g,isSVG:o}),x.current&&v.current&&(v.current.manuallyAnimateOnMount=!0));const b=v.current,C=q.useContext(F9);b&&!b.projection&&a&&(b.type==="html"||b.type==="svg")&&iw(v.current,e,a,C);const T=q.useRef(!1);q.useInsertionEffect(()=>{b&&T.current&&b.update(e,f)});const S=e[j7],L=q.useRef(!!S&&typeof window<"u"&&!window.MotionHandoffIsComplete?.(S)&&window.MotionHasOptimisedAnimation?.(S));return jl(()=>{x.current=!0,b&&(T.current=!0,window.MotionIsMounted=!0,b.updateFeatures(),b.scheduleRenderMicrotask(),L.current&&b.animationState&&b.animationState.animateChanges())}),q.useEffect(()=>{b&&(!L.current&&b.animationState&&b.animationState.animateChanges(),L.current&&(queueMicrotask(()=>{window.MotionHandoffMarkAsComplete?.(S)}),L.current=!1),b.enteringChildren=void 0)}),b}function iw(n,t,e,i){const{layoutId:a,layout:o,drag:c,dragConstraints:u,layoutScroll:f,layoutRoot:d,layoutAnchor:m,layoutCrossfade:g}=t;n.projection=new e(n.latestValues,t["data-framer-portal-id"]?void 0:U9(n.parent)),n.projection.setOptions({layoutId:a,layout:o,alwaysMeasureLayout:!!c||u&&Na(u),visualElement:n,animationType:typeof o=="string"?o:"both",initialPromotionConfig:i,crossfade:g,layoutScroll:f,layoutRoot:d,layoutAnchor:m})}function U9(n){if(n)return n.options.allowProjection!==!1?n.projection:U9(n.parent)}function gd(n,{forwardMotionProps:t=!1,type:e}={},i,a){i&&UE(i);const o=e?e==="svg":b2(n),c=o?JE:QE;function u(d,m){let g;const v={...q.useContext(Ja),...d,layoutId:sw(d)},{isStatic:x}=v,b=jE(d),C=c(d,x);if(!x&&typeof window<"u"){rw();const T=aw(v);g=T.MeasureLayout,b.visualElement=nw(n,C,v,a,T.ProjectionNode,o)}return Y.jsxs(Ah.Provider,{value:b,children:[g&&b.visualElement?Y.jsx(g,{visualElement:b.visualElement,...v}):null,WE(n,d,ew(C,b.visualElement,m),C,x,t,o)]})}u.displayName=`motion.${typeof n=="string"?n:`create(${n.displayName??n.name??""})`}`;const f=q.forwardRef(u);return f[tw]=n,f}function sw({layoutId:n}){const t=q.useContext(Nl).id;return t&&n!==void 0?t+"-"+n:n}function rw(n,t){q.useContext(N9).strict}function aw(n){const t=O9(),{drag:e,layout:i}=t;if(!e&&!i)return{};const a={...e,...i};return{MeasureLayout:e?.isEnabled(n)||i?.isEnabled(n)?a.MeasureLayout:void 0,ProjectionNode:a.ProjectionNode}}function ow(n,t){if(typeof Proxy>"u")return gd;const e=new Map,i=(o,c)=>gd(o,c,n,t),a=(o,c)=>i(o,c);return new Proxy(a,{get:(o,c)=>c==="create"?i:(e.has(c)||e.set(c,gd(c,void 0,n,t)),e.get(c))})}const lw=(n,t)=>t.isSVG??b2(n)?new g9(t):new h9(t,{allowProjection:n!==q.Fragment});class cw extends js{constructor(t){super(t),t.animationState||(t.animationState=FA(t))}updateAnimationControlsSubscription(){const{animate:t}=this.node.getProps();Sh(t)&&(this.unmountControls=t.subscribe(this.node))}mount(){this.updateAnimationControlsSubscription()}update(){const{animate:t}=this.node.getProps(),{animate:e}=this.node.prevProps||{};t!==e&&this.updateAnimationControlsSubscription()}unmount(){this.node.animationState.reset(),this.unmountControls?.()}}let uw=0;class hw extends js{constructor(){super(...arguments),this.id=uw++,this.isExitComplete=!1}update(){if(!this.node.presenceContext)return;const{isPresent:t,onExitComplete:e}=this.node.presenceContext,{isPresent:i}=this.node.prevPresenceContext||{};if(!this.node.animationState||t===i)return;if(t&&i===!1){if(this.isExitComplete){const{initial:o,custom:c}=this.node.getProps();if(typeof o=="string"||typeof o=="object"&&o!==null&&!Array.isArray(o)){const u=Rr(this.node,o,c);if(u){const{transition:f,transitionEnd:d,...m}=u;for(const g in m)this.node.getValue(g)?.jump(m[g])}}this.node.animationState.reset(),this.node.animationState.animateChanges()}else this.node.animationState.setActive("exit",!1);this.isExitComplete=!1;return}const a=this.node.animationState.setActive("exit",!t);e&&!t&&a.then(()=>{this.isExitComplete=!0,e(this.id)})}mount(){const{register:t,onExitComplete:e}=this.node.presenceContext||{};e&&e(this.id),t&&(this.unmount=t(this.id))}unmount(){}}const fw={animation:{Feature:cw},exit:{Feature:hw}};function Yl(n){return{point:{x:n.pageX,y:n.pageY}}}const dw=n=>t=>d2(t)&&n(t,Yl(t));function Al(n,t,e,i){return Fl(n,t,dw(e),i)}const k9=({current:n})=>n?n.ownerDocument.defaultView:null,b5=(n,t)=>Math.abs(n-t);function mw(n,t){const e=b5(n.x,t.x),i=b5(n.y,t.y);return Math.sqrt(e**2+i**2)}const x5=new Set(["auto","scroll"]);class z9{constructor(t,e,{transformPagePoint:i,contextWindow:a=window,dragSnapToOrigin:o=!1,distanceThreshold:c=3,element:u}={}){if(this.startEvent=null,this.lastMoveEvent=null,this.lastMoveEventInfo=null,this.lastRawMoveEventInfo=null,this.handlers={},this.contextWindow=window,this.scrollPositions=new Map,this.removeScrollListeners=null,this.onElementScroll=b=>{this.handleScroll(b.target)},this.onWindowScroll=()=>{this.handleScroll(window)},this.updatePoint=()=>{if(!(this.lastMoveEvent&&this.lastMoveEventInfo))return;this.lastRawMoveEventInfo&&(this.lastMoveEventInfo=mu(this.lastRawMoveEventInfo,this.transformPagePoint));const b=yd(this.lastMoveEventInfo,this.history),C=this.startEvent!==null,T=mw(b.offset,{x:0,y:0})>=this.distanceThreshold;if(!C&&!T)return;const{point:S}=b,{timestamp:L}=nn;this.history.push({...S,timestamp:L});const{onStart:R,onMove:P}=this.handlers;C||(R&&R(this.lastMoveEvent,b),this.startEvent=this.lastMoveEvent),P&&P(this.lastMoveEvent,b)},this.handlePointerMove=(b,C)=>{this.lastMoveEvent=b,this.lastRawMoveEventInfo=C,this.lastMoveEventInfo=mu(C,this.transformPagePoint),re.update(this.updatePoint,!0)},this.handlePointerUp=(b,C)=>{this.end();const{onEnd:T,onSessionEnd:S,resumeAnimation:L}=this.handlers;if((this.dragSnapToOrigin||!this.startEvent)&&L&&L(),!(this.lastMoveEvent&&this.lastMoveEventInfo))return;const R=yd(b.type==="pointercancel"?this.lastMoveEventInfo:mu(C,this.transformPagePoint),this.history);this.startEvent&&T&&T(b,R),S&&S(b,R)},!d2(t))return;this.dragSnapToOrigin=o,this.handlers=e,this.transformPagePoint=i,this.distanceThreshold=c,this.contextWindow=a||window;const f=Yl(t),d=mu(f,this.transformPagePoint),{point:m}=d,{timestamp:g}=nn;this.history=[{...m,timestamp:g}];const{onSessionStart:v}=e;v&&v(t,yd(d,this.history));const x={passive:!0,capture:!0};this.removeListeners=Hl(Al(this.contextWindow,"pointermove",this.handlePointerMove,x),Al(this.contextWindow,"pointerup",this.handlePointerUp,x),Al(this.contextWindow,"pointercancel",this.handlePointerUp,x)),u&&this.startScrollTracking(u)}startScrollTracking(t){let e=t.parentElement;for(;e;){const i=getComputedStyle(e);(x5.has(i.overflowX)||x5.has(i.overflowY))&&this.scrollPositions.set(e,{x:e.scrollLeft,y:e.scrollTop}),e=e.parentElement}this.scrollPositions.set(window,{x:window.scrollX,y:window.scrollY}),window.addEventListener("scroll",this.onElementScroll,{capture:!0}),window.addEventListener("scroll",this.onWindowScroll),this.removeScrollListeners=()=>{window.removeEventListener("scroll",this.onElementScroll,{capture:!0}),window.removeEventListener("scroll",this.onWindowScroll)}}handleScroll(t){const e=this.scrollPositions.get(t);if(!e)return;const i=t===window,a=i?{x:window.scrollX,y:window.scrollY}:{x:t.scrollLeft,y:t.scrollTop},o={x:a.x-e.x,y:a.y-e.y};o.x===0&&o.y===0||(i?this.lastMoveEventInfo&&(this.lastMoveEventInfo.point.x+=o.x,this.lastMoveEventInfo.point.y+=o.y):this.history.length>0&&(this.history[0].x-=o.x,this.history[0].y-=o.y),this.scrollPositions.set(t,a),re.update(this.updatePoint,!0))}updateHandlers(t){this.handlers=t}end(){this.removeListeners&&this.removeListeners(),this.removeScrollListeners&&this.removeScrollListeners(),this.scrollPositions.clear(),wi(this.updatePoint)}}function mu(n,t){return t?{point:t(n.point)}:n}function T5(n,t){return{x:n.x-t.x,y:n.y-t.y}}function yd({point:n},t){return{point:n,delta:T5(n,V9(t)),offset:T5(n,pw(t)),velocity:gw(t,.1)}}function pw(n){return n[0]}function V9(n){return n[n.length-1]}function gw(n,t){if(n.length<2)return{x:0,y:0};let e=n.length-1,i=null;const a=V9(n);for(;e>=0&&(i=n[e],!(a.timestamp-i.timestamp>Cn(t)));)e--;if(!i)return{x:0,y:0};i===n[0]&&n.length>2&&a.timestamp-i.timestamp>Cn(t)*2&&(i=n[1]);const o=Xn(a.timestamp-i.timestamp);if(o===0)return{x:0,y:0};const c={x:(a.x-i.x)/o,y:(a.y-i.y)/o};return c.x===1/0&&(c.x=0),c.y===1/0&&(c.y=0),c}function yw(n,{min:t,max:e},i){return t!==void 0&&ne&&(n=i?ye(e,n,i.max):Math.min(n,e)),n}function C5(n,t,e){return{min:t!==void 0?n.min+t:void 0,max:e!==void 0?n.max+e-(n.max-n.min):void 0}}function vw(n,{top:t,left:e,bottom:i,right:a}){return{x:C5(n.x,e,a),y:C5(n.y,t,i)}}function S5(n,t){let e=t.min-n.min,i=t.max-n.max;return t.max-t.mini?e=Xa(t.min,t.max-i,n.min):i>a&&(e=Xa(n.min,n.max-a,t.min)),Ei(0,1,e)}function Tw(n,t){const e={};return t.min!==void 0&&(e.min=t.min-n.min),t.max!==void 0&&(e.max=t.max-n.min),e}const dm=.35;function Cw(n=dm){return n===!1?n=0:n===!0&&(n=dm),{x:_5(n,"left","right"),y:_5(n,"top","bottom")}}function _5(n,t,e){return{min:A5(n,t),max:A5(n,e)}}function A5(n,t){return typeof n=="number"?n:n[t]||0}const Sw=new WeakMap;class _w{constructor(t){this.openDragLock=null,this.isDragging=!1,this.currentDirection=null,this.originPoint={x:0,y:0},this.constraints=!1,this.hasMutatedConstraints=!1,this.elastic=ze(),this.latestPointerEvent=null,this.latestPanInfo=null,this.visualElement=t}start(t,{snapToCursor:e=!1,distanceThreshold:i}={}){const{presenceContext:a}=this.visualElement;if(a&&a.isPresent===!1)return;const o=g=>{e&&this.snapToCursor(Yl(g).point),this.stopAnimation()},c=(g,v)=>{const{drag:x,dragPropagation:b,onDragStart:C}=this.getProps();if(x&&!b&&(this.openDragLock&&this.openDragLock(),this.openDragLock=X_(x),!this.openDragLock))return;this.latestPointerEvent=g,this.latestPanInfo=v,this.isDragging=!0,this.currentDirection=null,this.resolveConstraints(),this.visualElement.projection&&(this.visualElement.projection.isAnimationBlocked=!0,this.visualElement.projection.target=void 0),xi(S=>{let L=this.getAxisMotionValue(S).get()||0;if(Si.test(L)){const{projection:R}=this.visualElement;if(R&&R.layout){const P=R.layout.layoutBox[S];P&&(L=mn(P)*(parseFloat(L)/100))}}this.originPoint[S]=L}),C&&re.update(()=>C(g,v),!1,!0),im(this.visualElement,"transform");const{animationState:T}=this.visualElement;T&&T.setActive("whileDrag",!0)},u=(g,v)=>{this.latestPointerEvent=g,this.latestPanInfo=v;const{dragPropagation:x,dragDirectionLock:b,onDirectionLock:C,onDrag:T}=this.getProps();if(!x&&!this.openDragLock)return;const{offset:S}=v;if(b&&this.currentDirection===null){this.currentDirection=Ew(S),this.currentDirection!==null&&C&&C(this.currentDirection);return}this.updateAxis("x",v.point,S),this.updateAxis("y",v.point,S),this.visualElement.render(),T&&re.update(()=>T(g,v),!1,!0)},f=(g,v)=>{this.latestPointerEvent=g,this.latestPanInfo=v,this.stop(g,v),this.latestPointerEvent=null,this.latestPanInfo=null},d=()=>{const{dragSnapToOrigin:g}=this.getProps();(g||this.constraints)&&this.startAnimation({x:0,y:0})},{dragSnapToOrigin:m}=this.getProps();this.panSession=new z9(t,{onSessionStart:o,onStart:c,onMove:u,onSessionEnd:f,resumeAnimation:d},{transformPagePoint:this.visualElement.getTransformPagePoint(),dragSnapToOrigin:m,distanceThreshold:i,contextWindow:k9(this.visualElement),element:this.visualElement.current})}stop(t,e){const i=t||this.latestPointerEvent,a=e||this.latestPanInfo,o=this.isDragging;if(this.cancel(),!o||!a||!i)return;const{velocity:c}=a;this.startAnimation(c);const{onDragEnd:u}=this.getProps();u&&re.postRender(()=>u(i,a))}cancel(){this.isDragging=!1;const{projection:t,animationState:e}=this.visualElement;t&&(t.isAnimationBlocked=!1),this.endPanSession();const{dragPropagation:i}=this.getProps();!i&&this.openDragLock&&(this.openDragLock(),this.openDragLock=null),e&&e.setActive("whileDrag",!1)}endPanSession(){this.panSession&&this.panSession.end(),this.panSession=void 0}updateAxis(t,e,i){const{drag:a}=this.getProps();if(!i||!pu(t,a,this.currentDirection))return;const o=this.getAxisMotionValue(t);let c=this.originPoint[t]+i[t];this.constraints&&this.constraints[t]&&(c=yw(c,this.constraints[t],this.elastic[t])),o.set(c)}resolveConstraints(){const{dragConstraints:t,dragElastic:e}=this.getProps(),i=this.visualElement.projection&&!this.visualElement.projection.layout?this.visualElement.projection.measure(!1):this.visualElement.projection?.layout,a=this.constraints;t&&Na(t)?this.constraints||(this.constraints=this.resolveRefConstraints()):t&&i?this.constraints=vw(i.layoutBox,t):this.constraints=!1,this.elastic=Cw(e),a!==this.constraints&&!Na(t)&&i&&this.constraints&&!this.hasMutatedConstraints&&xi(o=>{this.constraints!==!1&&this.getAxisMotionValue(o)&&(this.constraints[o]=Tw(i.layoutBox[o],this.constraints[o]))})}resolveRefConstraints(){const{dragConstraints:t,onMeasureDragConstraints:e}=this.getProps();if(!t||!Na(t))return!1;const i=t.current,{projection:a}=this.visualElement;if(!a||!a.layout)return!1;a.root&&(a.root.scroll=void 0,a.root.updateScroll());const o=xA(i,a.root,this.visualElement.getTransformPagePoint());let c=bw(a.layout.layoutBox,o);if(e){const u=e(yA(c));this.hasMutatedConstraints=!!u,u&&(c=r9(u))}return c}startAnimation(t){const{drag:e,dragMomentum:i,dragElastic:a,dragTransition:o,dragSnapToOrigin:c,onDragTransitionEnd:u}=this.getProps(),f=this.constraints||{},d=xi(m=>{if(!pu(m,e,this.currentDirection))return;let g=f&&f[m]||{};(c===!0||c===m)&&(g={min:0,max:0});const v=a?200:1e6,x=a?40:1e7,b={type:"inertia",velocity:i?t[m]:0,bounceStiffness:v,bounceDamping:x,timeConstant:750,restDelta:1,restSpeed:10,...o,...g};return this.startAxisValueAnimation(m,b)});return Promise.all(d).then(u)}startAxisValueAnimation(t,e){const i=this.getAxisMotionValue(t);return im(this.visualElement,t),i.start(a2(t,i,0,e,this.visualElement,!1))}stopAnimation(){xi(t=>this.getAxisMotionValue(t).stop())}getAxisMotionValue(t){const e=`_drag${t.toUpperCase()}`,a=this.visualElement.getProps()[e];return a||this.visualElement.getValue(t,this.visualElement.latestValues[t]??0)}snapToCursor(t){xi(e=>{const{drag:i}=this.getProps();if(!pu(e,i,this.currentDirection))return;const{projection:a}=this.visualElement,o=this.getAxisMotionValue(e);if(a&&a.layout){const{min:c,max:u}=a.layout.layoutBox[e],f=o.get()||0;o.set(t[e]-ye(c,u,.5)+f)}})}scalePositionWithinConstraints(){if(!this.visualElement.current)return;const{drag:t,dragConstraints:e}=this.getProps(),{projection:i}=this.visualElement;if(!Na(e)||!i||!this.constraints)return;this.stopAnimation();const a={x:0,y:0};xi(c=>{const u=this.getAxisMotionValue(c);if(u&&this.constraints!==!1){const f=u.get();a[c]=xw({min:f,max:f},this.constraints[c])}});const{transformTemplate:o}=this.visualElement.getProps();this.visualElement.current.style.transform=o?o({},""):"none",i.root&&i.root.updateScroll(),i.updateLayout(),this.constraints=!1,this.resolveConstraints(),xi(c=>{if(!pu(c,t,null))return;const u=this.getAxisMotionValue(c),{min:f,max:d}=this.constraints[c];u.set(ye(f,d,a[c]))}),this.visualElement.render()}addListeners(){if(!this.visualElement.current)return;Sw.set(this.visualElement,this);const t=this.visualElement.current,e=Al(t,"pointerdown",d=>{const{drag:m,dragListener:g=!0}=this.getProps(),v=d.target,x=v!==t&&Q_(v);m&&g&&!x&&this.start(d)});let i;const a=()=>{const{dragConstraints:d}=this.getProps();Na(d)&&d.current&&(this.constraints=this.resolveRefConstraints(),i||(i=Aw(t,d.current,()=>this.scalePositionWithinConstraints())))},{projection:o}=this.visualElement,c=o.addEventListener("measure",a);o&&!o.layout&&(o.root&&o.root.updateScroll(),o.updateLayout()),re.read(a);const u=Fl(window,"resize",()=>this.scalePositionWithinConstraints()),f=o.addEventListener("didUpdate",(({delta:d,hasLayoutChanged:m})=>{this.isDragging&&m&&(xi(g=>{const v=this.getAxisMotionValue(g);v&&(this.originPoint[g]+=d[g].translate,v.set(v.get()+d[g].translate))}),this.visualElement.render())}));return()=>{u(),e(),c(),f&&f(),i&&i()}}getProps(){const t=this.visualElement.getProps(),{drag:e=!1,dragDirectionLock:i=!1,dragPropagation:a=!1,dragConstraints:o=!1,dragElastic:c=dm,dragMomentum:u=!0}=t;return{...t,drag:e,dragDirectionLock:i,dragPropagation:a,dragConstraints:o,dragElastic:c,dragMomentum:u}}}function E5(n){let t=!0;return()=>{if(t){t=!1;return}n()}}function Aw(n,t,e){const i=Ny(n,E5(e)),a=Ny(t,E5(e));return()=>{i(),a()}}function pu(n,t,e){return(t===!0||t===n)&&(e===null||e===n)}function Ew(n,t=10){let e=null;return Math.abs(n.y)>t?e="y":Math.abs(n.x)>t&&(e="x"),e}class ww extends js{constructor(t){super(t),this.removeGroupControls=Zn,this.removeListeners=Zn,this.controls=new _w(t)}mount(){const{dragControls:t}=this.node.getProps();t&&(this.removeGroupControls=t.subscribe(this.controls)),this.removeListeners=this.controls.addListeners()||Zn}update(){const{dragControls:t}=this.node.getProps(),{dragControls:e}=this.node.prevProps||{};t!==e&&(this.removeGroupControls(),t&&(this.removeGroupControls=t.subscribe(this.controls)))}unmount(){this.removeGroupControls(),this.removeListeners(),this.controls.isDragging||this.controls.endPanSession()}}const vd=n=>(t,e)=>{n&&re.update(()=>n(t,e),!1,!0)};class Mw extends js{constructor(){super(...arguments),this.removePointerDownListener=Zn}onPointerDown(t){this.session=new z9(t,this.createPanHandlers(),{transformPagePoint:this.node.getTransformPagePoint(),contextWindow:k9(this.node)})}createPanHandlers(){const{onPanSessionStart:t,onPanStart:e,onPan:i,onPanEnd:a}=this.node.getProps();return{onSessionStart:vd(t),onStart:vd(e),onMove:vd(i),onEnd:(o,c)=>{delete this.session,a&&re.postRender(()=>a(o,c))}}}mount(){this.removePointerDownListener=Al(this.node.current,"pointerdown",t=>this.onPointerDown(t))}update(){this.session&&this.session.updateHandlers(this.createPanHandlers())}unmount(){this.removePointerDownListener(),this.session&&this.session.end()}}let bd=!1;class Rw extends q.Component{componentDidMount(){const{visualElement:t,layoutGroup:e,switchLayoutGroup:i,layoutId:a}=this.props,{projection:o}=t;o&&(e.group&&e.group.add(o),i&&i.register&&a&&i.register(o),bd&&o.root.didUpdate(),o.addEventListener("animationComplete",()=>{this.safeToRemove()}),o.setOptions({...o.options,layoutDependency:this.props.layoutDependency,onExitComplete:()=>this.safeToRemove()})),ju.hasEverUpdated=!0}getSnapshotBeforeUpdate(t){const{layoutDependency:e,visualElement:i,drag:a,isPresent:o}=this.props,{projection:c}=i;return c&&(c.isPresent=o,t.layoutDependency!==e&&c.setOptions({...c.options,layoutDependency:e}),bd=!0,a||t.layoutDependency!==e||e===void 0||t.isPresent!==o?c.willUpdate():this.safeToRemove(),t.isPresent!==o&&(o?c.promote():c.relegate()||re.postRender(()=>{const u=c.getStack();(!u||!u.members.length)&&this.safeToRemove()}))),null}componentDidUpdate(){const{visualElement:t,layoutAnchor:e}=this.props,{projection:i}=t;i&&(i.options.layoutAnchor=e,i.root.didUpdate(),f2.postRender(()=>{!i.currentAnimation&&i.isLead()&&this.safeToRemove()}))}componentWillUnmount(){const{visualElement:t,layoutGroup:e,switchLayoutGroup:i}=this.props,{projection:a}=t;bd=!0,a&&(a.scheduleCheckAfterUnmount(),e&&e.group&&e.group.remove(a),i&&i.deregister&&i.deregister(a))}safeToRemove(){const{safeToRemove:t}=this.props;t&&t()}render(){return null}}function G9(n){const[t,e]=R9(),i=q.useContext(Nl);return Y.jsx(Rw,{...n,layoutGroup:i,switchLayoutGroup:q.useContext(F9),isPresent:t,safeToRemove:e})}const Lw={pan:{Feature:Mw},drag:{Feature:ww,ProjectionNode:M9,MeasureLayout:G9}};function w5(n,t,e){const{props:i}=n;n.animationState&&i.whileHover&&n.animationState.setActive("whileHover",e==="Start");const a="onHover"+e,o=i[a];o&&re.postRender(()=>o(t,Yl(t)))}class Nw extends js{mount(){const{current:t}=this.node;t&&(this.unmount=$_(t,(e,i)=>(w5(this.node,i,"Start"),a=>w5(this.node,a,"End"))))}unmount(){}}class Ow extends js{constructor(){super(...arguments),this.isActive=!1}onFocus(){let t=!1;try{t=this.node.current.matches(":focus-visible")}catch{t=!0}!t||!this.node.animationState||(this.node.animationState.setActive("whileFocus",!0),this.isActive=!0)}onBlur(){!this.isActive||!this.node.animationState||(this.node.animationState.setActive("whileFocus",!1),this.isActive=!1)}mount(){this.unmount=Hl(Fl(this.node.current,"focus",()=>this.onFocus()),Fl(this.node.current,"blur",()=>this.onBlur()))}unmount(){}}function M5(n,t,e){const{props:i}=n;if(n.current instanceof HTMLButtonElement&&n.current.disabled)return;n.animationState&&i.whileTap&&n.animationState.setActive("whileTap",e==="Start");const a="onTap"+(e==="End"?"":e),o=i[a];o&&re.postRender(()=>o(t,Yl(t)))}class Iw extends js{mount(){const{current:t}=this.node;if(!t)return;const{globalTapTarget:e,propagate:i}=this.node.props;this.unmount=tA(t,(a,o)=>(M5(this.node,o,"Start"),(c,{success:u})=>M5(this.node,c,u?"End":"Cancel")),{useGlobalTarget:e,stopPropagation:i?.tap===!1})}unmount(){}}const mm=new WeakMap,xd=new WeakMap,Dw=n=>{const t=mm.get(n.target);t&&t(n)},Bw=n=>{n.forEach(Dw)};function Pw({root:n,...t}){const e=n||document;xd.has(e)||xd.set(e,{});const i=xd.get(e),a=JSON.stringify(t);return i[a]||(i[a]=new IntersectionObserver(Bw,{root:n,...t})),i[a]}function Fw(n,t,e){const i=Pw(t);return mm.set(n,e),i.observe(n),()=>{mm.delete(n),i.unobserve(n)}}const Uw={some:0,all:1};class kw extends js{constructor(){super(...arguments),this.hasEnteredView=!1,this.isInView=!1}startObserver(){this.stopObserver?.();const{viewport:t={}}=this.node.getProps(),{root:e,margin:i,amount:a="some",once:o}=t,c={root:e?e.current:void 0,rootMargin:i,threshold:typeof a=="number"?a:Uw[a]},u=f=>{const{isIntersecting:d}=f;if(this.isInView===d||(this.isInView=d,o&&!d&&this.hasEnteredView))return;d&&(this.hasEnteredView=!0),this.node.animationState&&this.node.animationState.setActive("whileInView",d);const{onViewportEnter:m,onViewportLeave:g}=this.node.getProps(),v=d?m:g;v&&v(f)};this.stopObserver=Fw(this.node.current,c,u)}mount(){this.startObserver()}update(){if(typeof IntersectionObserver>"u")return;const{props:t,prevProps:e}=this.node;["amount","margin","root"].some(zw(t,e))&&this.startObserver()}unmount(){this.stopObserver?.(),this.hasEnteredView=!1,this.isInView=!1}}function zw({viewport:n={}},{viewport:t={}}={}){return e=>n[e]!==t[e]}const Vw={inView:{Feature:kw},tap:{Feature:Iw},focus:{Feature:Ow},hover:{Feature:Nw}},Gw={layout:{ProjectionNode:M9,MeasureLayout:G9}},jw={...fw,...Vw,...Lw,...Gw},Kn=ow(jw,lw);function oh(n){const t=Lr(()=>Vs(n)),{isStatic:e}=q.useContext(Ja);if(e){const[,i]=q.useState(n);q.useEffect(()=>t.on("change",i),[])}return t}function x2(n,t){const e=oh(t()),i=()=>e.set(t());return i(),jl(()=>{const a=()=>re.preRender(i,!1,!0),o=n.map(c=>c.on("change",a));return()=>{o.forEach(c=>c()),wi(i)}}),e}function Hw(n,...t){const e=n.length;function i(){let a="";for(let o=0;oc(d)),f=Array.isArray(n)?void 0:n.accelerate;return f&&!f.isTransformed&&typeof t!="function"&&Array.isArray(e)&&i?.clamp!==!1&&(u.accelerate={...f,times:t,keyframes:e,isTransformed:!0}),u}function R5(n,t){const e=Lr(()=>[]);return x2(n,()=>{e.length=0;const i=n.length;for(let a=0;aObject.keys(e)),o=Lr(()=>({}));for(const c of a)o[c]=T2(n,t,e[c],i);return o}function Yw(n,t={}){const{isStatic:e}=q.useContext(Ja),i=()=>Ae(n)?n.get():n;if(e)return T2(i);const a=oh(i());return q.useInsertionEffect(()=>uA(a,n,t),[a,JSON.stringify(t)]),a}function $w(n,t={}){return Yw(n,{type:"spring",...t})}function Ww(n){const t=q.useRef(0),{isStatic:e}=q.useContext(Ja);q.useEffect(()=>{if(e)return;const i=({timestamp:a,delta:o})=>{t.current||(t.current=a),n(a-t.current,o)};return re.update(i,!0),()=>wi(i)},[n])}function C2(n){return typeof n=="object"&&!Array.isArray(n)}function j9(n,t,e,i){return n==null?[]:typeof n=="string"&&C2(t)?h2(n,e,i):n instanceof NodeList?Array.from(n):Array.isArray(n)?n.filter(a=>a!=null):[n]}function Zw(n,t,e){return n*(t+1)+e*t}function L5(n,t,e,i){return typeof t=="number"?t:t.startsWith("-")||t.startsWith("+")?Math.max(0,n+parseFloat(t)):t==="<"?e:t.startsWith("<")?Math.max(0,e+parseFloat(t.slice(1))):i.get(t)??n}function Kw(n,t,e){for(let i=0;it&&a.at{const I=sM(P),{delay:k=0,times:U=M7(I),type:G=t.type||"keyframes",repeat:J,repeatType:tt,repeatDelay:K=0,...A}=w;let{ease:B=t.ease||"easeOut",duration:N}=w;const H=typeof k=="function"?k(E,O):k,V=I.length,M=s2(G)?G:a?.[G||"keyframes"];if(V<=2&&M){let st=100;if(V===2&&oM(I)){const pt=I[1]-I[0];st=Math.abs(pt)}const ct={...t,...A};N!==void 0&&(ct.duration=Cn(N));const ft=_7(ct,st,M);B=ft.ease,N=ft.duration}N??(N=o);const z=g+H;U.length===1&&U[0]===0&&(U[1]=1);const j=U.length-I.length;if(j>0&&w7(U,j),I.length===1&&I.unshift(null),J&&J0?K/N:0;N=Zw(N,J,K);const ct=[...I],ft=[...U];B=Array.isArray(B)?[...B]:[B];const pt=[...B],mt=tt==="reverse"||tt==="mirror";let dt=ct,St=pt;mt&&(dt=[...ct].reverse(),tt==="reverse"&&(St=[...pt].reverse().map(bt=>typeof bt=="function"?$m(bt):bt)));for(let bt=0;bt0&&(I.push(I[I.length-1]),U.push(xt),B.push("linear")),I.push(...wt);for(let ie=0;ie{for(const C in x){const T=x[C];T.sort(tM);const S=[],L=[],R=[];for(let E=0;Etypeof n=="number",oM=n=>n.every(aM);function lM(n){const t={presenceContext:null,props:{},visualState:{renderState:{transform:{},transformOrigin:{},style:{},vars:{},attrs:{}},latestValues:{}}},e=Ch(n)&&!J7(n)?new g9(t):new h9(t);e.mount(n),Bl.set(n,e)}function cM(n){const t={presenceContext:null,props:{},visualState:{renderState:{output:{}},latestValues:{}}},e=new wA(t);e.mount(n),Bl.set(n,e)}function uM(n,t){return Ae(n)||typeof n=="number"||typeof n=="string"&&!C2(t)}function H9(n,t,e,i){const a=[];if(uM(n,t))a.push(_9(n,C2(t)&&t.default||t,e&&(e.default||e)));else{if(n==null)return a;const o=j9(n,t,i),c=o.length;for(let u=0;u{if(Array.isArray(c)&&typeof c[0]=="function"){const u=c[0],f=Vs(0);return f.on("change",u),c.length===1?[f,[0,1]]:c.length===2?[f,[0,1],c[1]]:[f,c[1],c[2]]}return c});return iM(a,t,e,{spring:Il}).forEach(({keyframes:c,transition:u},f)=>{i.push(...H9(f,c,u))}),i}function fM(n){return Array.isArray(n)&&n.some(Array.isArray)}function dM(n={}){const{scope:t,reduceMotion:e,skipAnimations:i}=n;function a(o,c,u){let f=[],d;const m={};if(e!==void 0&&(m.reduceMotion=e),i!==void 0&&(m.skipAnimations=i),fM(o)){const{onComplete:v,...x}=c||{};typeof v=="function"&&(d=v),f=hM(o,{...m,...x},t)}else{const{onComplete:v,...x}=u||{};typeof v=="function"&&(d=v),f=H9(o,c,{...m,...x},t)}const g=new p_(f);return d&&g.finished.then(d),t&&(t.animations.push(g),g.finished.then(()=>{qa(t.animations,g)})),g}return a}const I5=dM();function mM({topLeftCornerRadius:n,topRightCornerRadius:t,bottomRightCornerRadius:e,bottomLeftCornerRadius:i,width:a,height:o}){const c={topLeft:-1,topRight:-1,bottomLeft:-1,bottomRight:-1},u={topLeft:n,topRight:t,bottomLeft:i,bottomRight:e};return Object.entries(u).sort(([,f],[,d])=>d-f).forEach(([f,d])=>{const m=f,g=pM[m],v=Math.min(...g.map(x=>{const b=u[x.corner];if(d===0&&b===0)return 0;const C=c[x.corner],T=x.side==="top"||x.side==="bottom"?a:o;return C>=0?T-c[x.corner]:d/(d+b)*T}));c[m]=v,u[m]=Math.min(d,v)}),{topLeft:{radius:u.topLeft,roundingAndSmoothingBudget:c.topLeft},topRight:{radius:u.topRight,roundingAndSmoothingBudget:c.topRight},bottomLeft:{radius:u.bottomLeft,roundingAndSmoothingBudget:c.bottomLeft},bottomRight:{radius:u.bottomRight,roundingAndSmoothingBudget:c.bottomRight}}}const pM={topLeft:[{corner:"topRight",side:"top"},{corner:"bottomLeft",side:"left"}],topRight:[{corner:"topLeft",side:"top"},{corner:"bottomRight",side:"right"}],bottomLeft:[{corner:"bottomRight",side:"bottom"},{corner:"topLeft",side:"left"}],bottomRight:[{corner:"bottomLeft",side:"bottom"},{corner:"topRight",side:"right"}]};function nl({cornerRadius:n,cornerSmoothing:t,preserveSmoothing:e,roundingAndSmoothingBudget:i}){let a=(1+t)*n;if(!e){const b=i/n-1;t=Math.min(t,b),a=Math.min(a,i)}const o=90*(1-t),c=Math.sin(gu(o/2))*n*Math.sqrt(2),u=(90-o)/2,f=n*Math.tan(gu(u/2)),d=45*t,m=f*Math.cos(gu(d)),g=m*Math.tan(gu(d));let v=(a-c-m-g)/3,x=2*v;if(e&&a>i){const b=i-g-c-m,C=b/6,T=b-C;v=Math.min(v,T),x=b-v,a=Math.min(a,i)}return{a:x,b:v,c:m,d:g,p:a,arcSectionLength:c,cornerRadius:n}}function D5({width:n,height:t,topLeftPathParams:e,topRightPathParams:i,bottomLeftPathParams:a,bottomRightPathParams:o}){return` + M ${n-i.p} 0 + ${gM(i)} + L ${n} ${t-o.p} + ${yM(o)} + L ${a.p} ${t} + ${vM(a)} + L 0 ${e.p} + ${bM(e)} + Z + `.replace(/[\t\s\n]+/g," ").trim()}function gM({cornerRadius:n,a:t,b:e,c:i,d:a,p:o,arcSectionLength:c}){return n?Gs` + c ${t} 0 ${t+e} 0 ${t+e+i} ${a} + a ${n} ${n} 0 0 1 ${c} ${c} + c ${a} ${i} + ${a} ${e+i} + ${a} ${t+e+i}`:Gs`l ${o} 0`}function yM({cornerRadius:n,a:t,b:e,c:i,d:a,p:o,arcSectionLength:c}){return n?Gs` + c 0 ${t} + 0 ${t+e} + ${-a} ${t+e+i} + a ${n} ${n} 0 0 1 -${c} ${c} + c ${-i} ${a} + ${-(e+i)} ${a} + ${-(t+e+i)} ${a}`:Gs`l 0 ${o}`}function vM({cornerRadius:n,a:t,b:e,c:i,d:a,p:o,arcSectionLength:c}){return n?Gs` + c ${-t} 0 + ${-(t+e)} 0 + ${-(t+e+i)} ${-a} + a ${n} ${n} 0 0 1 -${c} -${c} + c ${-a} ${-i} + ${-a} ${-(e+i)} + ${-a} ${-(t+e+i)}`:Gs`l ${-o} 0`}function bM({cornerRadius:n,a:t,b:e,c:i,d:a,p:o,arcSectionLength:c}){return n?Gs` + c 0 ${-t} + 0 ${-(t+e)} + ${a} ${-(t+e+i)} + a ${n} ${n} 0 0 1 ${c} -${c} + c ${i} ${-a} + ${e+i} ${-a} + ${t+e+i} ${-a}`:Gs`l 0 ${-o}`}function gu(n){return n*Math.PI/180}function Gs(n,...t){return n.reduce((e,i,a)=>{let o=t[a];return typeof o=="number"?e+i+o.toFixed(4):e+i+(o??"")},"")}function Td({cornerRadius:n=0,topLeftCornerRadius:t,topRightCornerRadius:e,bottomRightCornerRadius:i,bottomLeftCornerRadius:a,cornerSmoothing:o,width:c,height:u,preserveSmoothing:f=!1}){if(t=t??n,e=e??n,a=a??n,i=i??n,t===e&&e===i&&i===a&&a===t){const x=Math.min(c,u)/2,b=Math.min(t,x),C=nl({cornerRadius:b,cornerSmoothing:o,preserveSmoothing:f,roundingAndSmoothingBudget:x});return D5({width:c,height:u,topLeftPathParams:C,topRightPathParams:C,bottomLeftPathParams:C,bottomRightPathParams:C})}const{topLeft:d,topRight:m,bottomLeft:g,bottomRight:v}=mM({topLeftCornerRadius:t,topRightCornerRadius:e,bottomRightCornerRadius:i,bottomLeftCornerRadius:a,width:c,height:u});return D5({width:c,height:u,topLeftPathParams:nl({cornerSmoothing:o,preserveSmoothing:f,cornerRadius:d.radius,roundingAndSmoothingBudget:d.roundingAndSmoothingBudget}),topRightPathParams:nl({cornerSmoothing:o,preserveSmoothing:f,cornerRadius:m.radius,roundingAndSmoothingBudget:m.roundingAndSmoothingBudget}),bottomRightPathParams:nl({cornerSmoothing:o,preserveSmoothing:f,cornerRadius:v.radius,roundingAndSmoothingBudget:v.roundingAndSmoothingBudget}),bottomLeftPathParams:nl({cornerSmoothing:o,preserveSmoothing:f,cornerRadius:g.radius,roundingAndSmoothingBudget:g.roundingAndSmoothingBudget})})}const xM=(n=8)=>{const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";let e="";for(let i=0;i{let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,document.head.appendChild(e)),e.textContent=n},CM=(n,t,e)=>{const i=n.clientWidth,a=n.clientHeight,o={preserveSmoothing:!0,cornerSmoothing:1,width:i,height:a,...t};t.borderWidth?(n.classList.add(e),TM(` + .${e} { + position: relative; + clip-path: path('${Td(o)}'); + } + + .${e}::before { + content: ''; + display: block; + position: absolute; + inset: ${t.borderWidth}px; + z-index: -1; + clip-path: path('${Td({...o,width:i-t.borderWidth*2,height:a-t.borderWidth*2,cornerRadius:t.cornerRadius-t.borderWidth})}'); + } + `,`style-${e}`)):n.style.clipPath=`path('${Td(o)}')`},SM=(n,t)=>{const e=xM();let i;const a=c=>(c!==void 0&&(t=c),CM(n,t,e)),o=new ResizeObserver(()=>{const c=i;i=[n.clientWidth,n.clientHeight],(c?.[0]!==i[0]||c?.[1]!==i[1])&&a()});return o.observe(n),a.disconnect=()=>{n.classList.remove(e),document.querySelector(`#style-${e}`)?.remove(),o.disconnect()},a},_M=n=>{const t={};for(const e in n)n[e]!==void 0&&(t[e]=n[e]);return t},AM=q.forwardRef(({children:n,as:t="div",cornerRadius:e,topLeftCornerRadius:i,topRightCornerRadius:a,bottomRightCornerRadius:o,bottomLeftCornerRadius:c,cornerSmoothing:u,preserveSmoothing:f,borderWidth:d,...m},g)=>{const v=q.useRef(),x=q.useCallback(b=>{const C=_M({cornerRadius:e,topLeftCornerRadius:i,topRightCornerRadius:a,bottomRightCornerRadius:o,bottomLeftCornerRadius:c,cornerSmoothing:u,preserveSmoothing:f,borderWidth:d});v.current?.disconnect(),b&&(v.current=SM(b,C)),typeof g=="function"?g(b):g&&(g.current=b)},[e,i,a,o,c,u,f,d]);return Gl.createElement(t,{ref:x,...m},n)}),le=1e-6,B5=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);class Rt extends Float32Array{static BYTE_LENGTH=16*Float32Array.BYTES_PER_ELEMENT;constructor(...t){switch(t.length){case 16:super(t);break;case 2:super(t[0],t[1],16);break;case 1:const e=t[0];typeof e=="number"?super([e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e]):super(e,0,16);break;default:super(B5);break}}get str(){return Rt.str(this)}copy(t){return this.set(t),this}identity(){return this.set(B5),this}multiply(t){return Rt.multiply(this,this,t)}mul(t){return this}transpose(){return Rt.transpose(this,this)}invert(){return Rt.invert(this,this)}translate(t){return Rt.translate(this,this,t)}rotate(t,e){return Rt.rotate(this,this,t,e)}scale(t){return Rt.scale(this,this,t)}rotateX(t){return Rt.rotateX(this,this,t)}rotateY(t){return Rt.rotateY(this,this,t)}rotateZ(t){return Rt.rotateZ(this,this,t)}perspectiveNO(t,e,i,a){return Rt.perspectiveNO(this,t,e,i,a)}perspectiveZO(t,e,i,a){return Rt.perspectiveZO(this,t,e,i,a)}orthoNO(t,e,i,a,o,c){return Rt.orthoNO(this,t,e,i,a,o,c)}orthoZO(t,e,i,a,o,c){return Rt.orthoZO(this,t,e,i,a,o,c)}static create(){return new Rt}static clone(t){return new Rt(t)}static copy(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t[6]=e[6],t[7]=e[7],t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t}static fromValues(...t){return new Rt(...t)}static set(t,...e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t[6]=e[6],t[7]=e[7],t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t}static identity(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=1,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=1,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}static transpose(t,e){if(t===e){const i=e[1],a=e[2],o=e[3],c=e[6],u=e[7],f=e[11];t[1]=e[4],t[2]=e[8],t[3]=e[12],t[4]=i,t[6]=e[9],t[7]=e[13],t[8]=a,t[9]=c,t[11]=e[14],t[12]=o,t[13]=u,t[14]=f}else t[0]=e[0],t[1]=e[4],t[2]=e[8],t[3]=e[12],t[4]=e[1],t[5]=e[5],t[6]=e[9],t[7]=e[13],t[8]=e[2],t[9]=e[6],t[10]=e[10],t[11]=e[14],t[12]=e[3],t[13]=e[7],t[14]=e[11],t[15]=e[15];return t}static invert(t,e){const i=e[0],a=e[1],o=e[2],c=e[3],u=e[4],f=e[5],d=e[6],m=e[7],g=e[8],v=e[9],x=e[10],b=e[11],C=e[12],T=e[13],S=e[14],L=e[15],R=i*f-a*u,P=i*d-o*u,w=i*m-c*u,F=a*d-o*f,E=a*m-c*f,O=o*m-c*d,I=g*T-v*C,k=g*S-x*C,U=g*L-b*C,G=v*S-x*T,J=v*L-b*T,tt=x*L-b*S;let K=R*tt-P*J+w*G+F*U-E*k+O*I;return K?(K=1/K,t[0]=(f*tt-d*J+m*G)*K,t[1]=(o*J-a*tt-c*G)*K,t[2]=(T*O-S*E+L*F)*K,t[3]=(x*E-v*O-b*F)*K,t[4]=(d*U-u*tt-m*k)*K,t[5]=(i*tt-o*U+c*k)*K,t[6]=(S*w-C*O-L*P)*K,t[7]=(g*O-x*w+b*P)*K,t[8]=(u*J-f*U+m*I)*K,t[9]=(a*U-i*J-c*I)*K,t[10]=(C*E-T*w+L*R)*K,t[11]=(v*w-g*E-b*R)*K,t[12]=(f*k-u*G-d*I)*K,t[13]=(i*G-a*k+o*I)*K,t[14]=(T*P-C*F-S*R)*K,t[15]=(g*F-v*P+x*R)*K,t):null}static adjoint(t,e){const i=e[0],a=e[1],o=e[2],c=e[3],u=e[4],f=e[5],d=e[6],m=e[7],g=e[8],v=e[9],x=e[10],b=e[11],C=e[12],T=e[13],S=e[14],L=e[15],R=i*f-a*u,P=i*d-o*u,w=i*m-c*u,F=a*d-o*f,E=a*m-c*f,O=o*m-c*d,I=g*T-v*C,k=g*S-x*C,U=g*L-b*C,G=v*S-x*T,J=v*L-b*T,tt=x*L-b*S;return t[0]=f*tt-d*J+m*G,t[1]=o*J-a*tt-c*G,t[2]=T*O-S*E+L*F,t[3]=x*E-v*O-b*F,t[4]=d*U-u*tt-m*k,t[5]=i*tt-o*U+c*k,t[6]=S*w-C*O-L*P,t[7]=g*O-x*w+b*P,t[8]=u*J-f*U+m*I,t[9]=a*U-i*J-c*I,t[10]=C*E-T*w+L*R,t[11]=v*w-g*E-b*R,t[12]=f*k-u*G-d*I,t[13]=i*G-a*k+o*I,t[14]=T*P-C*F-S*R,t[15]=g*F-v*P+x*R,t}static determinant(t){const e=t[0],i=t[1],a=t[2],o=t[3],c=t[4],u=t[5],f=t[6],d=t[7],m=t[8],g=t[9],v=t[10],x=t[11],b=t[12],C=t[13],T=t[14],S=t[15],L=e*u-i*c,R=e*f-a*c,P=i*f-a*u,w=m*C-g*b,F=m*T-v*b,E=g*T-v*C,O=e*E-i*F+a*w,I=c*E-u*F+f*w,k=m*P-g*R+v*L,U=b*P-C*R+T*L;return d*O-o*I+S*k-x*U}static multiply(t,e,i){const a=e[0],o=e[1],c=e[2],u=e[3],f=e[4],d=e[5],m=e[6],g=e[7],v=e[8],x=e[9],b=e[10],C=e[11],T=e[12],S=e[13],L=e[14],R=e[15];let P=i[0],w=i[1],F=i[2],E=i[3];return t[0]=P*a+w*f+F*v+E*T,t[1]=P*o+w*d+F*x+E*S,t[2]=P*c+w*m+F*b+E*L,t[3]=P*u+w*g+F*C+E*R,P=i[4],w=i[5],F=i[6],E=i[7],t[4]=P*a+w*f+F*v+E*T,t[5]=P*o+w*d+F*x+E*S,t[6]=P*c+w*m+F*b+E*L,t[7]=P*u+w*g+F*C+E*R,P=i[8],w=i[9],F=i[10],E=i[11],t[8]=P*a+w*f+F*v+E*T,t[9]=P*o+w*d+F*x+E*S,t[10]=P*c+w*m+F*b+E*L,t[11]=P*u+w*g+F*C+E*R,P=i[12],w=i[13],F=i[14],E=i[15],t[12]=P*a+w*f+F*v+E*T,t[13]=P*o+w*d+F*x+E*S,t[14]=P*c+w*m+F*b+E*L,t[15]=P*u+w*g+F*C+E*R,t}static mul(t,e,i){return t}static translate(t,e,i){const a=i[0],o=i[1],c=i[2];if(e===t)t[12]=e[0]*a+e[4]*o+e[8]*c+e[12],t[13]=e[1]*a+e[5]*o+e[9]*c+e[13],t[14]=e[2]*a+e[6]*o+e[10]*c+e[14],t[15]=e[3]*a+e[7]*o+e[11]*c+e[15];else{const u=e[0],f=e[1],d=e[2],m=e[3],g=e[4],v=e[5],x=e[6],b=e[7],C=e[8],T=e[9],S=e[10],L=e[11];t[0]=u,t[1]=f,t[2]=d,t[3]=m,t[4]=g,t[5]=v,t[6]=x,t[7]=b,t[8]=C,t[9]=T,t[10]=S,t[11]=L,t[12]=u*a+g*o+C*c+e[12],t[13]=f*a+v*o+T*c+e[13],t[14]=d*a+x*o+S*c+e[14],t[15]=m*a+b*o+L*c+e[15]}return t}static scale(t,e,i){const a=i[0],o=i[1],c=i[2];return t[0]=e[0]*a,t[1]=e[1]*a,t[2]=e[2]*a,t[3]=e[3]*a,t[4]=e[4]*o,t[5]=e[5]*o,t[6]=e[6]*o,t[7]=e[7]*o,t[8]=e[8]*c,t[9]=e[9]*c,t[10]=e[10]*c,t[11]=e[11]*c,t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t}static rotate(t,e,i,a){let o=a[0],c=a[1],u=a[2],f=Math.sqrt(o*o+c*c+u*u);if(f0?(si[0]=(u*c+m*i+f*o-d*a)*2/g,si[1]=(f*c+m*a+d*i-u*o)*2/g,si[2]=(d*c+m*o+u*a-f*i)*2/g):(si[0]=(u*c+m*i+f*o-d*a)*2,si[1]=(f*c+m*a+d*i-u*o)*2,si[2]=(d*c+m*o+u*a-f*i)*2),Rt.fromRotationTranslation(t,e,si),t}static normalFromMat4(t,e){const i=e[0],a=e[1],o=e[2],c=e[3],u=e[4],f=e[5],d=e[6],m=e[7],g=e[8],v=e[9],x=e[10],b=e[11],C=e[12],T=e[13],S=e[14],L=e[15],R=i*f-a*u,P=i*d-o*u,w=i*m-c*u,F=a*d-o*f,E=a*m-c*f,O=o*m-c*d,I=g*T-v*C,k=g*S-x*C,U=g*L-b*C,G=v*S-x*T,J=v*L-b*T,tt=x*L-b*S;let K=R*tt-P*J+w*G+F*U-E*k+O*I;return K?(K=1/K,t[0]=(f*tt-d*J+m*G)*K,t[1]=(d*U-u*tt-m*k)*K,t[2]=(u*J-f*U+m*I)*K,t[3]=0,t[4]=(o*J-a*tt-c*G)*K,t[5]=(i*tt-o*U+c*k)*K,t[6]=(a*U-i*J-c*I)*K,t[7]=0,t[8]=(T*O-S*E+L*F)*K,t[9]=(S*w-C*O-L*P)*K,t[10]=(C*E-T*w+L*R)*K,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t):null}static normalFromMat4Fast(t,e){const i=e[0],a=e[1],o=e[2],c=e[4],u=e[5],f=e[6],d=e[8],m=e[9],g=e[10];return t[0]=u*g-g*m,t[1]=f*d-d*g,t[2]=c*m-m*d,t[3]=0,t[4]=m*o-g*a,t[5]=g*i-d*o,t[6]=d*a-m*i,t[7]=0,t[8]=a*f-o*u,t[9]=o*c-i*f,t[10]=i*u-a*c,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}static getTranslation(t,e){return t[0]=e[12],t[1]=e[13],t[2]=e[14],t}static getScaling(t,e){const i=e[0],a=e[1],o=e[2],c=e[4],u=e[5],f=e[6],d=e[8],m=e[9],g=e[10];return t[0]=Math.sqrt(i*i+a*a+o*o),t[1]=Math.sqrt(c*c+u*u+f*f),t[2]=Math.sqrt(d*d+m*m+g*g),t}static getRotation(t,e){Rt.getScaling(si,e);const i=1/si[0],a=1/si[1],o=1/si[2],c=e[0]*i,u=e[1]*a,f=e[2]*o,d=e[4]*i,m=e[5]*a,g=e[6]*o,v=e[8]*i,x=e[9]*a,b=e[10]*o,C=c+m+b;let T=0;return C>0?(T=Math.sqrt(C+1)*2,t[3]=.25*T,t[0]=(g-x)/T,t[1]=(v-f)/T,t[2]=(u-d)/T):c>m&&c>b?(T=Math.sqrt(1+c-m-b)*2,t[3]=(g-x)/T,t[0]=.25*T,t[1]=(u+d)/T,t[2]=(v+f)/T):m>b?(T=Math.sqrt(1+m-c-b)*2,t[3]=(v-f)/T,t[0]=(u+d)/T,t[1]=.25*T,t[2]=(g+x)/T):(T=Math.sqrt(1+b-c-m)*2,t[3]=(u-d)/T,t[0]=(v+f)/T,t[1]=(g+x)/T,t[2]=.25*T),t}static decompose(t,e,i,a){e[0]=a[12],e[1]=a[13],e[2]=a[14];const o=a[0],c=a[1],u=a[2],f=a[4],d=a[5],m=a[6],g=a[8],v=a[9],x=a[10];i[0]=Math.sqrt(o*o+c*c+u*u),i[1]=Math.sqrt(f*f+d*d+m*m),i[2]=Math.sqrt(g*g+v*v+x*x);const b=1/i[0],C=1/i[1],T=1/i[2],S=o*b,L=c*C,R=u*T,P=f*b,w=d*C,F=m*T,E=g*b,O=v*C,I=x*T,k=S+w+I;let U=0;return k>0?(U=Math.sqrt(k+1)*2,t[3]=.25*U,t[0]=(F-O)/U,t[1]=(E-R)/U,t[2]=(L-P)/U):S>w&&S>I?(U=Math.sqrt(1+S-w-I)*2,t[3]=(F-O)/U,t[0]=.25*U,t[1]=(L+P)/U,t[2]=(E+R)/U):w>I?(U=Math.sqrt(1+w-S-I)*2,t[3]=(E-R)/U,t[0]=(L+P)/U,t[1]=.25*U,t[2]=(F+O)/U):(U=Math.sqrt(1+I-S-w)*2,t[3]=(L-P)/U,t[0]=(E+R)/U,t[1]=(F+O)/U,t[2]=.25*U),t}static fromRotationTranslationScale(t,e,i,a){const o=e[0],c=e[1],u=e[2],f=e[3],d=o+o,m=c+c,g=u+u,v=o*d,x=o*m,b=o*g,C=c*m,T=c*g,S=u*g,L=f*d,R=f*m,P=f*g,w=a[0],F=a[1],E=a[2];return t[0]=(1-(C+S))*w,t[1]=(x+P)*w,t[2]=(b-R)*w,t[3]=0,t[4]=(x-P)*F,t[5]=(1-(v+S))*F,t[6]=(T+L)*F,t[7]=0,t[8]=(b+R)*E,t[9]=(T-L)*E,t[10]=(1-(v+C))*E,t[11]=0,t[12]=i[0],t[13]=i[1],t[14]=i[2],t[15]=1,t}static fromRotationTranslationScaleOrigin(t,e,i,a,o){const c=e[0],u=e[1],f=e[2],d=e[3],m=c+c,g=u+u,v=f+f,x=c*m,b=c*g,C=c*v,T=u*g,S=u*v,L=f*v,R=d*m,P=d*g,w=d*v,F=a[0],E=a[1],O=a[2],I=o[0],k=o[1],U=o[2],G=(1-(T+L))*F,J=(b+w)*F,tt=(C-P)*F,K=(b-w)*E,A=(1-(x+L))*E,B=(S+R)*E,N=(C+P)*O,H=(S-R)*O,V=(1-(x+T))*O;return t[0]=G,t[1]=J,t[2]=tt,t[3]=0,t[4]=K,t[5]=A,t[6]=B,t[7]=0,t[8]=N,t[9]=H,t[10]=V,t[11]=0,t[12]=i[0]+I-(G*I+K*k+N*U),t[13]=i[1]+k-(J*I+A*k+H*U),t[14]=i[2]+U-(tt*I+B*k+V*U),t[15]=1,t}static fromQuat(t,e){const i=e[0],a=e[1],o=e[2],c=e[3],u=i+i,f=a+a,d=o+o,m=i*u,g=a*u,v=a*f,x=o*u,b=o*f,C=o*d,T=c*u,S=c*f,L=c*d;return t[0]=1-v-C,t[1]=g+L,t[2]=x-S,t[3]=0,t[4]=g-L,t[5]=1-m-C,t[6]=b+T,t[7]=0,t[8]=x+S,t[9]=b-T,t[10]=1-m-v,t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t}static frustumNO(t,e,i,a,o,c,u=1/0){const f=1/(i-e),d=1/(o-a);if(t[0]=c*2*f,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c*2*d,t[6]=0,t[7]=0,t[8]=(i+e)*f,t[9]=(o+a)*d,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,u!=null&&u!==1/0){const m=1/(c-u);t[10]=(u+c)*m,t[14]=2*u*c*m}else t[10]=-1,t[14]=-2*c;return t}static frustum(t,e,i,a,o,c,u=1/0){return t}static frustumZO(t,e,i,a,o,c,u=1/0){const f=1/(i-e),d=1/(o-a);if(t[0]=c*2*f,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c*2*d,t[6]=0,t[7]=0,t[8]=(i+e)*f,t[9]=(o+a)*d,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,u!=null&&u!==1/0){const m=1/(c-u);t[10]=u*m,t[14]=u*c*m}else t[10]=-1,t[14]=-c;return t}static perspectiveNO(t,e,i,a,o=1/0){const c=1/Math.tan(e/2);if(t[0]=c/i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,o!=null&&o!==1/0){const u=1/(a-o);t[10]=(o+a)*u,t[14]=2*o*a*u}else t[10]=-1,t[14]=-2*a;return t}static perspective(t,e,i,a,o=1/0){return t}static perspectiveZO(t,e,i,a,o=1/0){const c=1/Math.tan(e/2);if(t[0]=c/i,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=c,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,o!=null&&o!==1/0){const u=1/(a-o);t[10]=o*u,t[14]=o*a*u}else t[10]=-1,t[14]=-a;return t}static perspectiveFromFieldOfView(t,e,i,a){const o=Math.tan(e.upDegrees*Math.PI/180),c=Math.tan(e.downDegrees*Math.PI/180),u=Math.tan(e.leftDegrees*Math.PI/180),f=Math.tan(e.rightDegrees*Math.PI/180),d=2/(u+f),m=2/(o+c);return t[0]=d,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=m,t[6]=0,t[7]=0,t[8]=-((u-f)*d*.5),t[9]=(o-c)*m*.5,t[10]=a/(i-a),t[11]=-1,t[12]=0,t[13]=0,t[14]=a*i/(i-a),t[15]=0,t}static orthoNO(t,e,i,a,o,c,u){const f=1/(e-i),d=1/(a-o),m=1/(c-u);return t[0]=-2*f,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*d,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=2*m,t[11]=0,t[12]=(e+i)*f,t[13]=(o+a)*d,t[14]=(u+c)*m,t[15]=1,t}static ortho(t,e,i,a,o,c,u){return t}static orthoZO(t,e,i,a,o,c,u){const f=1/(e-i),d=1/(a-o),m=1/(c-u);return t[0]=-2*f,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=-2*d,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=m,t[11]=0,t[12]=(e+i)*f,t[13]=(o+a)*d,t[14]=c*m,t[15]=1,t}static lookAt(t,e,i,a){const o=e[0],c=e[1],u=e[2],f=a[0],d=a[1],m=a[2],g=i[0],v=i[1],x=i[2];if(Math.abs(o-g)0&&(b=1/Math.sqrt(b),g*=b,v*=b,x*=b);let C=d*x-m*v,T=m*g-f*x,S=f*v-d*g;return b=C*C+T*T+S*S,b>0&&(b=1/Math.sqrt(b),C*=b,T*=b,S*=b),t[0]=C,t[1]=T,t[2]=S,t[3]=0,t[4]=v*S-x*T,t[5]=x*C-g*S,t[6]=g*T-v*C,t[7]=0,t[8]=g,t[9]=v,t[10]=x,t[11]=0,t[12]=o,t[13]=c,t[14]=u,t[15]=1,t}static frob(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]+t[3]*t[3]+t[4]*t[4]+t[5]*t[5]+t[6]*t[6]+t[7]*t[7]+t[8]*t[8]+t[9]*t[9]+t[10]*t[10]+t[11]*t[11]+t[12]*t[12]+t[13]*t[13]+t[14]*t[14]+t[15]*t[15])}static add(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t[3]=e[3]+i[3],t[4]=e[4]+i[4],t[5]=e[5]+i[5],t[6]=e[6]+i[6],t[7]=e[7]+i[7],t[8]=e[8]+i[8],t[9]=e[9]+i[9],t[10]=e[10]+i[10],t[11]=e[11]+i[11],t[12]=e[12]+i[12],t[13]=e[13]+i[13],t[14]=e[14]+i[14],t[15]=e[15]+i[15],t}static subtract(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t[3]=e[3]-i[3],t[4]=e[4]-i[4],t[5]=e[5]-i[5],t[6]=e[6]-i[6],t[7]=e[7]-i[7],t[8]=e[8]-i[8],t[9]=e[9]-i[9],t[10]=e[10]-i[10],t[11]=e[11]-i[11],t[12]=e[12]-i[12],t[13]=e[13]-i[13],t[14]=e[14]-i[14],t[15]=e[15]-i[15],t}static sub(t,e,i){return t}static multiplyScalar(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t[2]=e[2]*i,t[3]=e[3]*i,t[4]=e[4]*i,t[5]=e[5]*i,t[6]=e[6]*i,t[7]=e[7]*i,t[8]=e[8]*i,t[9]=e[9]*i,t[10]=e[10]*i,t[11]=e[11]*i,t[12]=e[12]*i,t[13]=e[13]*i,t[14]=e[14]*i,t[15]=e[15]*i,t}static multiplyScalarAndAdd(t,e,i,a){return t[0]=e[0]+i[0]*a,t[1]=e[1]+i[1]*a,t[2]=e[2]+i[2]*a,t[3]=e[3]+i[3]*a,t[4]=e[4]+i[4]*a,t[5]=e[5]+i[5]*a,t[6]=e[6]+i[6]*a,t[7]=e[7]+i[7]*a,t[8]=e[8]+i[8]*a,t[9]=e[9]+i[9]*a,t[10]=e[10]+i[10]*a,t[11]=e[11]+i[11]*a,t[12]=e[12]+i[12]*a,t[13]=e[13]+i[13]*a,t[14]=e[14]+i[14]*a,t[15]=e[15]+i[15]*a,t}static exactEquals(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]&&t[3]===e[3]&&t[4]===e[4]&&t[5]===e[5]&&t[6]===e[6]&&t[7]===e[7]&&t[8]===e[8]&&t[9]===e[9]&&t[10]===e[10]&&t[11]===e[11]&&t[12]===e[12]&&t[13]===e[13]&&t[14]===e[14]&&t[15]===e[15]}static equals(t,e){const i=t[0],a=t[1],o=t[2],c=t[3],u=t[4],f=t[5],d=t[6],m=t[7],g=t[8],v=t[9],x=t[10],b=t[11],C=t[12],T=t[13],S=t[14],L=t[15],R=e[0],P=e[1],w=e[2],F=e[3],E=e[4],O=e[5],I=e[6],k=e[7],U=e[8],G=e[9],J=e[10],tt=e[11],K=e[12],A=e[13],B=e[14],N=e[15];return Math.abs(i-R)<=le*Math.max(1,Math.abs(i),Math.abs(R))&&Math.abs(a-P)<=le*Math.max(1,Math.abs(a),Math.abs(P))&&Math.abs(o-w)<=le*Math.max(1,Math.abs(o),Math.abs(w))&&Math.abs(c-F)<=le*Math.max(1,Math.abs(c),Math.abs(F))&&Math.abs(u-E)<=le*Math.max(1,Math.abs(u),Math.abs(E))&&Math.abs(f-O)<=le*Math.max(1,Math.abs(f),Math.abs(O))&&Math.abs(d-I)<=le*Math.max(1,Math.abs(d),Math.abs(I))&&Math.abs(m-k)<=le*Math.max(1,Math.abs(m),Math.abs(k))&&Math.abs(g-U)<=le*Math.max(1,Math.abs(g),Math.abs(U))&&Math.abs(v-G)<=le*Math.max(1,Math.abs(v),Math.abs(G))&&Math.abs(x-J)<=le*Math.max(1,Math.abs(x),Math.abs(J))&&Math.abs(b-tt)<=le*Math.max(1,Math.abs(b),Math.abs(tt))&&Math.abs(C-K)<=le*Math.max(1,Math.abs(C),Math.abs(K))&&Math.abs(T-A)<=le*Math.max(1,Math.abs(T),Math.abs(A))&&Math.abs(S-B)<=le*Math.max(1,Math.abs(S),Math.abs(B))&&Math.abs(L-N)<=le*Math.max(1,Math.abs(L),Math.abs(N))}static str(t){return`Mat4(${t.join(", ")})`}}const si=new Float32Array(3);Rt.prototype.mul=Rt.prototype.multiply;Rt.sub=Rt.subtract;Rt.mul=Rt.multiply;Rt.frustum=Rt.frustumNO;Rt.perspective=Rt.perspectiveNO;Rt.ortho=Rt.orthoNO;class kt extends Float32Array{static BYTE_LENGTH=3*Float32Array.BYTES_PER_ELEMENT;constructor(...t){switch(t.length){case 3:super(t);break;case 2:super(t[0],t[1],3);break;case 1:{const e=t[0];typeof e=="number"?super([e,e,e]):super(e,0,3);break}default:super(3);break}}get x(){return this[0]}set x(t){this[0]=t}get y(){return this[1]}set y(t){this[1]=t}get z(){return this[2]}set z(t){this[2]=t}get r(){return this[0]}set r(t){this[0]=t}get g(){return this[1]}set g(t){this[1]=t}get b(){return this[2]}set b(t){this[2]=t}get magnitude(){const t=this[0],e=this[1],i=this[2];return Math.sqrt(t*t+e*e+i*i)}get mag(){return this.magnitude}get squaredMagnitude(){const t=this[0],e=this[1],i=this[2];return t*t+e*e+i*i}get sqrMag(){return this.squaredMagnitude}get str(){return kt.str(this)}copy(t){return this.set(t),this}add(t){return this[0]+=t[0],this[1]+=t[1],this[2]+=t[2],this}subtract(t){return this[0]-=t[0],this[1]-=t[1],this[2]-=t[2],this}sub(t){return this}multiply(t){return this[0]*=t[0],this[1]*=t[1],this[2]*=t[2],this}mul(t){return this}divide(t){return this[0]/=t[0],this[1]/=t[1],this[2]/=t[2],this}div(t){return this}scale(t){return this[0]*=t,this[1]*=t,this[2]*=t,this}scaleAndAdd(t,e){return this[0]+=t[0]*e,this[1]+=t[1]*e,this[2]+=t[2]*e,this}distance(t){return kt.distance(this,t)}dist(t){return 0}squaredDistance(t){return kt.squaredDistance(this,t)}sqrDist(t){return 0}negate(){return this[0]*=-1,this[1]*=-1,this[2]*=-1,this}invert(){return this[0]=1/this[0],this[1]=1/this[1],this[2]=1/this[2],this}abs(){return this[0]=Math.abs(this[0]),this[1]=Math.abs(this[1]),this[2]=Math.abs(this[2]),this}dot(t){return this[0]*t[0]+this[1]*t[1]+this[2]*t[2]}normalize(){return kt.normalize(this,this)}static create(){return new kt}static clone(t){return new kt(t)}static magnitude(t){let e=t[0],i=t[1],a=t[2];return Math.sqrt(e*e+i*i+a*a)}static mag(t){return 0}static length(t){return 0}static len(t){return 0}static fromValues(t,e,i){return new kt(t,e,i)}static copy(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t}static set(t,e,i,a){return t[0]=e,t[1]=i,t[2]=a,t}static add(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t}static subtract(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t}static sub(t,e,i){return[0,0,0]}static multiply(t,e,i){return t[0]=e[0]*i[0],t[1]=e[1]*i[1],t[2]=e[2]*i[2],t}static mul(t,e,i){return[0,0,0]}static divide(t,e,i){return t[0]=e[0]/i[0],t[1]=e[1]/i[1],t[2]=e[2]/i[2],t}static div(t,e,i){return[0,0,0]}static ceil(t,e){return t[0]=Math.ceil(e[0]),t[1]=Math.ceil(e[1]),t[2]=Math.ceil(e[2]),t}static floor(t,e){return t[0]=Math.floor(e[0]),t[1]=Math.floor(e[1]),t[2]=Math.floor(e[2]),t}static min(t,e,i){return t[0]=Math.min(e[0],i[0]),t[1]=Math.min(e[1],i[1]),t[2]=Math.min(e[2],i[2]),t}static max(t,e,i){return t[0]=Math.max(e[0],i[0]),t[1]=Math.max(e[1],i[1]),t[2]=Math.max(e[2],i[2]),t}static scale(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t[2]=e[2]*i,t}static scaleAndAdd(t,e,i,a){return t[0]=e[0]+i[0]*a,t[1]=e[1]+i[1]*a,t[2]=e[2]+i[2]*a,t}static distance(t,e){const i=e[0]-t[0],a=e[1]-t[1],o=e[2]-t[2];return Math.sqrt(i*i+a*a+o*o)}static dist(t,e){return 0}static squaredDistance(t,e){const i=e[0]-t[0],a=e[1]-t[1],o=e[2]-t[2];return i*i+a*a+o*o}static sqrDist(t,e){return 0}static squaredLength(t){const e=t[0],i=t[1],a=t[2];return e*e+i*i+a*a}static sqrLen(t,e){return 0}static negate(t,e){return t[0]=-e[0],t[1]=-e[1],t[2]=-e[2],t}static inverse(t,e){return t[0]=1/e[0],t[1]=1/e[1],t[2]=1/e[2],t}static abs(t,e){return t[0]=Math.abs(e[0]),t[1]=Math.abs(e[1]),t[2]=Math.abs(e[2]),t}static normalize(t,e){const i=e[0],a=e[1],o=e[2];let c=i*i+a*a+o*o;return c>0&&(c=1/Math.sqrt(c)),t[0]=e[0]*c,t[1]=e[1]*c,t[2]=e[2]*c,t}static dot(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}static cross(t,e,i){const a=e[0],o=e[1],c=e[2],u=i[0],f=i[1],d=i[2];return t[0]=o*d-c*f,t[1]=c*u-a*d,t[2]=a*f-o*u,t}static lerp(t,e,i,a){const o=e[0],c=e[1],u=e[2];return t[0]=o+a*(i[0]-o),t[1]=c+a*(i[1]-c),t[2]=u+a*(i[2]-u),t}static slerp(t,e,i,a){const o=Math.acos(Math.min(Math.max(kt.dot(e,i),-1),1)),c=Math.sin(o),u=Math.sin((1-a)*o)/c,f=Math.sin(a*o)/c;return t[0]=u*e[0]+f*i[0],t[1]=u*e[1]+f*i[1],t[2]=u*e[2]+f*i[2],t}static hermite(t,e,i,a,o,c){const u=c*c,f=u*(2*c-3)+1,d=u*(c-2)+c,m=u*(c-1),g=u*(3-2*c);return t[0]=e[0]*f+i[0]*d+a[0]*m+o[0]*g,t[1]=e[1]*f+i[1]*d+a[1]*m+o[1]*g,t[2]=e[2]*f+i[2]*d+a[2]*m+o[2]*g,t}static bezier(t,e,i,a,o,c){const u=1-c,f=u*u,d=c*c,m=f*u,g=3*c*f,v=3*d*u,x=d*c;return t[0]=e[0]*m+i[0]*g+a[0]*v+o[0]*x,t[1]=e[1]*m+i[1]*g+a[1]*v+o[1]*x,t[2]=e[2]*m+i[2]*g+a[2]*v+o[2]*x,t}static transformMat4(t,e,i){const a=e[0],o=e[1],c=e[2],u=i[3]*a+i[7]*o+i[11]*c+i[15]||1;return t[0]=(i[0]*a+i[4]*o+i[8]*c+i[12])/u,t[1]=(i[1]*a+i[5]*o+i[9]*c+i[13])/u,t[2]=(i[2]*a+i[6]*o+i[10]*c+i[14])/u,t}static transformMat3(t,e,i){let a=e[0],o=e[1],c=e[2];return t[0]=a*i[0]+o*i[3]+c*i[6],t[1]=a*i[1]+o*i[4]+c*i[7],t[2]=a*i[2]+o*i[5]+c*i[8],t}static transformQuat(t,e,i){const a=i[0],o=i[1],c=i[2],u=i[3]*2,f=e[0],d=e[1],m=e[2],g=o*m-c*d,v=c*f-a*m,x=a*d-o*f,b=(o*x-c*v)*2,C=(c*g-a*x)*2,T=(a*v-o*g)*2;return t[0]=f+g*u+b,t[1]=d+v*u+C,t[2]=m+x*u+T,t}static rotateX(t,e,i,a){const o=i[1],c=i[2],u=e[1]-o,f=e[2]-c;return t[0]=e[0],t[1]=u*Math.cos(a)-f*Math.sin(a)+o,t[2]=u*Math.sin(a)+f*Math.cos(a)+c,t}static rotateY(t,e,i,a){const o=i[0],c=i[2],u=e[0]-o,f=e[2]-c;return t[0]=f*Math.sin(a)+u*Math.cos(a)+o,t[1]=e[1],t[2]=f*Math.cos(a)-u*Math.sin(a)+c,t}static rotateZ(t,e,i,a){const o=i[0],c=i[1],u=e[0]-o,f=e[1]-c;return t[0]=u*Math.cos(a)-f*Math.sin(a)+o,t[1]=u*Math.sin(a)+f*Math.cos(a)+c,t[2]=i[2],t}static angle(t,e){const i=t[0],a=t[1],o=t[2],c=e[0],u=e[1],f=e[2],d=Math.sqrt((i*i+a*a+o*o)*(c*c+u*u+f*f)),m=d&&kt.dot(t,e)/d;return Math.acos(Math.min(Math.max(m,-1),1))}static zero(t){return t[0]=0,t[1]=0,t[2]=0,t}static str(t){return`Vec3(${t.join(", ")})`}static exactEquals(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]}static equals(t,e){const i=t[0],a=t[1],o=t[2],c=e[0],u=e[1],f=e[2];return Math.abs(i-c)<=le*Math.max(1,Math.abs(i),Math.abs(c))&&Math.abs(a-u)<=le*Math.max(1,Math.abs(a),Math.abs(u))&&Math.abs(o-f)<=le*Math.max(1,Math.abs(o),Math.abs(f))}}kt.prototype.sub=kt.prototype.subtract;kt.prototype.mul=kt.prototype.multiply;kt.prototype.div=kt.prototype.divide;kt.prototype.dist=kt.prototype.distance;kt.prototype.sqrDist=kt.prototype.squaredDistance;kt.sub=kt.subtract;kt.mul=kt.multiply;kt.div=kt.divide;kt.dist=kt.distance;kt.sqrDist=kt.squaredDistance;kt.sqrLen=kt.squaredLength;kt.mag=kt.magnitude;kt.length=kt.magnitude;kt.len=kt.magnitude;class Ot extends Float32Array{static BYTE_LENGTH=4*Float32Array.BYTES_PER_ELEMENT;constructor(...t){switch(t.length){case 4:super(t);break;case 2:super(t[0],t[1],4);break;case 1:{const e=t[0];typeof e=="number"?super([e,e,e,e]):super(e,0,4);break}default:super(4);break}}get x(){return this[0]}set x(t){this[0]=t}get y(){return this[1]}set y(t){this[1]=t}get z(){return this[2]}set z(t){this[2]=t}get w(){return this[3]}set w(t){this[3]=t}get r(){return this[0]}set r(t){this[0]=t}get g(){return this[1]}set g(t){this[1]=t}get b(){return this[2]}set b(t){this[2]=t}get a(){return this[3]}set a(t){this[3]=t}get magnitude(){const t=this[0],e=this[1],i=this[2],a=this[3];return Math.sqrt(t*t+e*e+i*i+a*a)}get mag(){return this.magnitude}get str(){return Ot.str(this)}copy(t){return super.set(t),this}add(t){return this[0]+=t[0],this[1]+=t[1],this[2]+=t[2],this[3]+=t[3],this}subtract(t){return this[0]-=t[0],this[1]-=t[1],this[2]-=t[2],this[3]-=t[3],this}sub(t){return this}multiply(t){return this[0]*=t[0],this[1]*=t[1],this[2]*=t[2],this[3]*=t[3],this}mul(t){return this}divide(t){return this[0]/=t[0],this[1]/=t[1],this[2]/=t[2],this[3]/=t[3],this}div(t){return this}scale(t){return this[0]*=t,this[1]*=t,this[2]*=t,this[3]*=t,this}scaleAndAdd(t,e){return this[0]+=t[0]*e,this[1]+=t[1]*e,this[2]+=t[2]*e,this[3]+=t[3]*e,this}distance(t){return Ot.distance(this,t)}dist(t){return 0}squaredDistance(t){return Ot.squaredDistance(this,t)}sqrDist(t){return 0}negate(){return this[0]*=-1,this[1]*=-1,this[2]*=-1,this[3]*=-1,this}invert(){return this[0]=1/this[0],this[1]=1/this[1],this[2]=1/this[2],this[3]=1/this[3],this}abs(){return this[0]=Math.abs(this[0]),this[1]=Math.abs(this[1]),this[2]=Math.abs(this[2]),this[3]=Math.abs(this[3]),this}dot(t){return this[0]*t[0]+this[1]*t[1]+this[2]*t[2]+this[3]*t[3]}normalize(){return Ot.normalize(this,this)}static create(){return new Ot}static clone(t){return new Ot(t)}static fromValues(t,e,i,a){return new Ot(t,e,i,a)}static copy(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}static set(t,e,i,a,o){return t[0]=e,t[1]=i,t[2]=a,t[3]=o,t}static add(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t[3]=e[3]+i[3],t}static subtract(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t[3]=e[3]-i[3],t}static sub(t,e,i){return t}static multiply(t,e,i){return t[0]=e[0]*i[0],t[1]=e[1]*i[1],t[2]=e[2]*i[2],t[3]=e[3]*i[3],t}static mul(t,e,i){return t}static divide(t,e,i){return t[0]=e[0]/i[0],t[1]=e[1]/i[1],t[2]=e[2]/i[2],t[3]=e[3]/i[3],t}static div(t,e,i){return t}static ceil(t,e){return t[0]=Math.ceil(e[0]),t[1]=Math.ceil(e[1]),t[2]=Math.ceil(e[2]),t[3]=Math.ceil(e[3]),t}static floor(t,e){return t[0]=Math.floor(e[0]),t[1]=Math.floor(e[1]),t[2]=Math.floor(e[2]),t[3]=Math.floor(e[3]),t}static min(t,e,i){return t[0]=Math.min(e[0],i[0]),t[1]=Math.min(e[1],i[1]),t[2]=Math.min(e[2],i[2]),t[3]=Math.min(e[3],i[3]),t}static max(t,e,i){return t[0]=Math.max(e[0],i[0]),t[1]=Math.max(e[1],i[1]),t[2]=Math.max(e[2],i[2]),t[3]=Math.max(e[3],i[3]),t}static round(t,e){return t[0]=Math.round(e[0]),t[1]=Math.round(e[1]),t[2]=Math.round(e[2]),t[3]=Math.round(e[3]),t}static scale(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t[2]=e[2]*i,t[3]=e[3]*i,t}static scaleAndAdd(t,e,i,a){return t[0]=e[0]+i[0]*a,t[1]=e[1]+i[1]*a,t[2]=e[2]+i[2]*a,t[3]=e[3]+i[3]*a,t}static distance(t,e){const i=e[0]-t[0],a=e[1]-t[1],o=e[2]-t[2],c=e[3]-t[3];return Math.hypot(i,a,o,c)}static dist(t,e){return 0}static squaredDistance(t,e){const i=e[0]-t[0],a=e[1]-t[1],o=e[2]-t[2],c=e[3]-t[3];return i*i+a*a+o*o+c*c}static sqrDist(t,e){return 0}static magnitude(t){const e=t[0],i=t[1],a=t[2],o=t[3];return Math.sqrt(e*e+i*i+a*a+o*o)}static mag(t){return 0}static length(t){return 0}static len(t){return 0}static squaredLength(t){const e=t[0],i=t[1],a=t[2],o=t[3];return e*e+i*i+a*a+o*o}static sqrLen(t){return 0}static negate(t,e){return t[0]=-e[0],t[1]=-e[1],t[2]=-e[2],t[3]=-e[3],t}static inverse(t,e){return t[0]=1/e[0],t[1]=1/e[1],t[2]=1/e[2],t[3]=1/e[3],t}static abs(t,e){return t[0]=Math.abs(e[0]),t[1]=Math.abs(e[1]),t[2]=Math.abs(e[2]),t[3]=Math.abs(e[3]),t}static normalize(t,e){const i=e[0],a=e[1],o=e[2],c=e[3];let u=i*i+a*a+o*o+c*c;return u>0&&(u=1/Math.sqrt(u)),t[0]=i*u,t[1]=a*u,t[2]=o*u,t[3]=c*u,t}static dot(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3]}static cross(t,e,i,a){const o=i[0]*a[1]-i[1]*a[0],c=i[0]*a[2]-i[2]*a[0],u=i[0]*a[3]-i[3]*a[0],f=i[1]*a[2]-i[2]*a[1],d=i[1]*a[3]-i[3]*a[1],m=i[2]*a[3]-i[3]*a[2],g=e[0],v=e[1],x=e[2],b=e[3];return t[0]=v*m-x*d+b*f,t[1]=-(g*m)+x*u-b*c,t[2]=g*d-v*u+b*o,t[3]=-(g*f)+v*c-x*o,t}static lerp(t,e,i,a){const o=e[0],c=e[1],u=e[2],f=e[3];return t[0]=o+a*(i[0]-o),t[1]=c+a*(i[1]-c),t[2]=u+a*(i[2]-u),t[3]=f+a*(i[3]-f),t}static transformMat4(t,e,i){const a=e[0],o=e[1],c=e[2],u=e[3];return t[0]=i[0]*a+i[4]*o+i[8]*c+i[12]*u,t[1]=i[1]*a+i[5]*o+i[9]*c+i[13]*u,t[2]=i[2]*a+i[6]*o+i[10]*c+i[14]*u,t[3]=i[3]*a+i[7]*o+i[11]*c+i[15]*u,t}static transformQuat(t,e,i){const a=e[0],o=e[1],c=e[2],u=i[0],f=i[1],d=i[2],m=i[3],g=m*a+f*c-d*o,v=m*o+d*a-u*c,x=m*c+u*o-f*a,b=-u*a-f*o-d*c;return t[0]=g*m+b*-u+v*-d-x*-f,t[1]=v*m+b*-f+x*-u-g*-d,t[2]=x*m+b*-d+g*-f-v*-u,t[3]=e[3],t}static zero(t){return t[0]=0,t[1]=0,t[2]=0,t[3]=0,t}static str(t){return`Vec4(${t.join(", ")})`}static exactEquals(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]&&t[3]===e[3]}static equals(t,e){const i=t[0],a=t[1],o=t[2],c=t[3],u=e[0],f=e[1],d=e[2],m=e[3];return Math.abs(i-u)<=le*Math.max(1,Math.abs(i),Math.abs(u))&&Math.abs(a-f)<=le*Math.max(1,Math.abs(a),Math.abs(f))&&Math.abs(o-d)<=le*Math.max(1,Math.abs(o),Math.abs(d))&&Math.abs(c-m)<=le*Math.max(1,Math.abs(c),Math.abs(m))}}Ot.prototype.sub=Ot.prototype.subtract;Ot.prototype.mul=Ot.prototype.multiply;Ot.prototype.div=Ot.prototype.divide;Ot.prototype.dist=Ot.prototype.distance;Ot.prototype.sqrDist=Ot.prototype.squaredDistance;Ot.sub=Ot.subtract;Ot.mul=Ot.multiply;Ot.div=Ot.divide;Ot.dist=Ot.distance;Ot.sqrDist=Ot.squaredDistance;Ot.sqrLen=Ot.squaredLength;Ot.mag=Ot.magnitude;Ot.length=Ot.magnitude;Ot.len=Ot.magnitude;class Ft extends Float32Array{static BYTE_LENGTH=2*Float32Array.BYTES_PER_ELEMENT;constructor(...t){switch(t.length){case 2:{const e=t[0];typeof e=="number"?super([e,t[1]]):super(e,t[1],2);break}case 1:{const e=t[0];typeof e=="number"?super([e,e]):super(e,0,2);break}default:super(2);break}}get x(){return this[0]}set x(t){this[0]=t}get y(){return this[1]}set y(t){this[1]=t}get r(){return this[0]}set r(t){this[0]=t}get g(){return this[1]}set g(t){this[1]=t}get magnitude(){return Math.hypot(this[0],this[1])}get mag(){return this.magnitude}get squaredMagnitude(){const t=this[0],e=this[1];return t*t+e*e}get sqrMag(){return this.squaredMagnitude}get str(){return Ft.str(this)}copy(t){return this.set(t),this}add(t){return this[0]+=t[0],this[1]+=t[1],this}subtract(t){return this[0]-=t[0],this[1]-=t[1],this}sub(t){return this}multiply(t){return this[0]*=t[0],this[1]*=t[1],this}mul(t){return this}divide(t){return this[0]/=t[0],this[1]/=t[1],this}div(t){return this}scale(t){return this[0]*=t,this[1]*=t,this}scaleAndAdd(t,e){return this[0]+=t[0]*e,this[1]+=t[1]*e,this}distance(t){return Ft.distance(this,t)}dist(t){return 0}squaredDistance(t){return Ft.squaredDistance(this,t)}sqrDist(t){return 0}negate(){return this[0]*=-1,this[1]*=-1,this}invert(){return this[0]=1/this[0],this[1]=1/this[1],this}abs(){return this[0]=Math.abs(this[0]),this[1]=Math.abs(this[1]),this}dot(t){return this[0]*t[0]+this[1]*t[1]}normalize(){return Ft.normalize(this,this)}static create(){return new Ft}static clone(t){return new Ft(t)}static fromValues(t,e){return new Ft(t,e)}static copy(t,e){return t[0]=e[0],t[1]=e[1],t}static set(t,e,i){return t[0]=e,t[1]=i,t}static add(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t}static subtract(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t}static sub(t,e,i){return[0,0]}static multiply(t,e,i){return t[0]=e[0]*i[0],t[1]=e[1]*i[1],t}static mul(t,e,i){return[0,0]}static divide(t,e,i){return t[0]=e[0]/i[0],t[1]=e[1]/i[1],t}static div(t,e,i){return[0,0]}static ceil(t,e){return t[0]=Math.ceil(e[0]),t[1]=Math.ceil(e[1]),t}static floor(t,e){return t[0]=Math.floor(e[0]),t[1]=Math.floor(e[1]),t}static min(t,e,i){return t[0]=Math.min(e[0],i[0]),t[1]=Math.min(e[1],i[1]),t}static max(t,e,i){return t[0]=Math.max(e[0],i[0]),t[1]=Math.max(e[1],i[1]),t}static round(t,e){return t[0]=Math.round(e[0]),t[1]=Math.round(e[1]),t}static scale(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t}static scaleAndAdd(t,e,i,a){return t[0]=e[0]+i[0]*a,t[1]=e[1]+i[1]*a,t}static distance(t,e){return Math.hypot(e[0]-t[0],e[1]-t[1])}static dist(t,e){return 0}static squaredDistance(t,e){const i=e[0]-t[0],a=e[1]-t[1];return i*i+a*a}static sqrDist(t,e){return 0}static magnitude(t){let e=t[0],i=t[1];return Math.sqrt(e*e+i*i)}static mag(t){return 0}static length(t){return 0}static len(t){return 0}static squaredLength(t){const e=t[0],i=t[1];return e*e+i*i}static sqrLen(t,e){return 0}static negate(t,e){return t[0]=-e[0],t[1]=-e[1],t}static inverse(t,e){return t[0]=1/e[0],t[1]=1/e[1],t}static abs(t,e){return t[0]=Math.abs(e[0]),t[1]=Math.abs(e[1]),t}static normalize(t,e){const i=e[0],a=e[1];let o=i*i+a*a;return o>0&&(o=1/Math.sqrt(o)),t[0]=e[0]*o,t[1]=e[1]*o,t}static dot(t,e){return t[0]*e[0]+t[1]*e[1]}static cross(t,e,i){const a=e[0]*i[1]-e[1]*i[0];return t[0]=t[1]=0,t[2]=a,t}static lerp(t,e,i,a){const o=e[0],c=e[1];return t[0]=o+a*(i[0]-o),t[1]=c+a*(i[1]-c),t}static transformMat2(t,e,i){const a=e[0],o=e[1];return t[0]=i[0]*a+i[2]*o,t[1]=i[1]*a+i[3]*o,t}static transformMat2d(t,e,i){const a=e[0],o=e[1];return t[0]=i[0]*a+i[2]*o+i[4],t[1]=i[1]*a+i[3]*o+i[5],t}static transformMat3(t,e,i){const a=e[0],o=e[1];return t[0]=i[0]*a+i[3]*o+i[6],t[1]=i[1]*a+i[4]*o+i[7],t}static transformMat4(t,e,i){const a=e[0],o=e[1];return t[0]=i[0]*a+i[4]*o+i[12],t[1]=i[1]*a+i[5]*o+i[13],t}static rotate(t,e,i,a){const o=e[0]-i[0],c=e[1]-i[1],u=Math.sin(a),f=Math.cos(a);return t[0]=o*f-c*u+i[0],t[1]=o*u+c*f+i[1],t}static angle(t,e){const i=t[0],a=t[1],o=e[0],c=e[1],u=Math.sqrt(i*i+a*a)*Math.sqrt(o*o+c*c),f=u&&(i*o+a*c)/u;return Math.acos(Math.min(Math.max(f,-1),1))}static zero(t){return t[0]=0,t[1]=0,t}static exactEquals(t,e){return t[0]===e[0]&&t[1]===e[1]}static equals(t,e){const i=t[0],a=t[1],o=e[0],c=e[1];return Math.abs(i-o)<=le*Math.max(1,Math.abs(i),Math.abs(o))&&Math.abs(a-c)<=le*Math.max(1,Math.abs(a),Math.abs(c))}static str(t){return`Vec2(${t.join(", ")})`}}Ft.prototype.sub=Ft.prototype.subtract;Ft.prototype.mul=Ft.prototype.multiply;Ft.prototype.div=Ft.prototype.divide;Ft.prototype.dist=Ft.prototype.distance;Ft.prototype.sqrDist=Ft.prototype.squaredDistance;Ft.sub=Ft.subtract;Ft.mul=Ft.multiply;Ft.div=Ft.divide;Ft.dist=Ft.distance;Ft.sqrDist=Ft.squaredDistance;Ft.sqrLen=Ft.squaredLength;Ft.mag=Ft.magnitude;Ft.length=Ft.magnitude;Ft.len=Ft.magnitude;var Br=(n=>(n[n.WEBGL_LEGACY=0]="WEBGL_LEGACY",n[n.WEBGL=1]="WEBGL",n[n.WEBGL2=2]="WEBGL2",n))(Br||{}),q9=(n=>(n[n.UNKNOWN=0]="UNKNOWN",n[n.WEBGL=1]="WEBGL",n[n.CANVAS=2]="CANVAS",n))(q9||{}),pm=(n=>(n[n.COLOR=16384]="COLOR",n[n.DEPTH=256]="DEPTH",n[n.STENCIL=1024]="STENCIL",n))(pm||{}),Nt=(n=>(n[n.NORMAL=0]="NORMAL",n[n.ADD=1]="ADD",n[n.MULTIPLY=2]="MULTIPLY",n[n.SCREEN=3]="SCREEN",n[n.OVERLAY=4]="OVERLAY",n[n.DARKEN=5]="DARKEN",n[n.LIGHTEN=6]="LIGHTEN",n[n.COLOR_DODGE=7]="COLOR_DODGE",n[n.COLOR_BURN=8]="COLOR_BURN",n[n.HARD_LIGHT=9]="HARD_LIGHT",n[n.SOFT_LIGHT=10]="SOFT_LIGHT",n[n.DIFFERENCE=11]="DIFFERENCE",n[n.EXCLUSION=12]="EXCLUSION",n[n.HUE=13]="HUE",n[n.SATURATION=14]="SATURATION",n[n.COLOR=15]="COLOR",n[n.LUMINOSITY=16]="LUMINOSITY",n[n.NORMAL_NPM=17]="NORMAL_NPM",n[n.ADD_NPM=18]="ADD_NPM",n[n.SCREEN_NPM=19]="SCREEN_NPM",n[n.NONE=20]="NONE",n[n.SRC_OVER=0]="SRC_OVER",n[n.SRC_IN=21]="SRC_IN",n[n.SRC_OUT=22]="SRC_OUT",n[n.SRC_ATOP=23]="SRC_ATOP",n[n.DST_OVER=24]="DST_OVER",n[n.DST_IN=25]="DST_IN",n[n.DST_OUT=26]="DST_OUT",n[n.DST_ATOP=27]="DST_ATOP",n[n.ERASE=26]="ERASE",n[n.SUBTRACT=28]="SUBTRACT",n[n.XOR=29]="XOR",n))(Nt||{}),lh=(n=>(n[n.POINTS=0]="POINTS",n[n.LINES=1]="LINES",n[n.LINE_LOOP=2]="LINE_LOOP",n[n.LINE_STRIP=3]="LINE_STRIP",n[n.TRIANGLES=4]="TRIANGLES",n[n.TRIANGLE_STRIP=5]="TRIANGLE_STRIP",n[n.TRIANGLE_FAN=6]="TRIANGLE_FAN",n))(lh||{}),gt=(n=>(n[n.RGBA=6408]="RGBA",n[n.RGB=6407]="RGB",n[n.RG=33319]="RG",n[n.RED=6403]="RED",n[n.RGBA_INTEGER=36249]="RGBA_INTEGER",n[n.RGB_INTEGER=36248]="RGB_INTEGER",n[n.RG_INTEGER=33320]="RG_INTEGER",n[n.RED_INTEGER=36244]="RED_INTEGER",n[n.ALPHA=6406]="ALPHA",n[n.LUMINANCE=6409]="LUMINANCE",n[n.LUMINANCE_ALPHA=6410]="LUMINANCE_ALPHA",n[n.DEPTH_COMPONENT=6402]="DEPTH_COMPONENT",n[n.DEPTH_STENCIL=34041]="DEPTH_STENCIL",n))(gt||{}),ja=(n=>(n[n.TEXTURE_2D=3553]="TEXTURE_2D",n[n.TEXTURE_CUBE_MAP=34067]="TEXTURE_CUBE_MAP",n[n.TEXTURE_2D_ARRAY=35866]="TEXTURE_2D_ARRAY",n[n.TEXTURE_CUBE_MAP_POSITIVE_X=34069]="TEXTURE_CUBE_MAP_POSITIVE_X",n[n.TEXTURE_CUBE_MAP_NEGATIVE_X=34070]="TEXTURE_CUBE_MAP_NEGATIVE_X",n[n.TEXTURE_CUBE_MAP_POSITIVE_Y=34071]="TEXTURE_CUBE_MAP_POSITIVE_Y",n[n.TEXTURE_CUBE_MAP_NEGATIVE_Y=34072]="TEXTURE_CUBE_MAP_NEGATIVE_Y",n[n.TEXTURE_CUBE_MAP_POSITIVE_Z=34073]="TEXTURE_CUBE_MAP_POSITIVE_Z",n[n.TEXTURE_CUBE_MAP_NEGATIVE_Z=34074]="TEXTURE_CUBE_MAP_NEGATIVE_Z",n))(ja||{}),jt=(n=>(n[n.UNSIGNED_BYTE=5121]="UNSIGNED_BYTE",n[n.UNSIGNED_SHORT=5123]="UNSIGNED_SHORT",n[n.UNSIGNED_SHORT_5_6_5=33635]="UNSIGNED_SHORT_5_6_5",n[n.UNSIGNED_SHORT_4_4_4_4=32819]="UNSIGNED_SHORT_4_4_4_4",n[n.UNSIGNED_SHORT_5_5_5_1=32820]="UNSIGNED_SHORT_5_5_5_1",n[n.UNSIGNED_INT=5125]="UNSIGNED_INT",n[n.UNSIGNED_INT_10F_11F_11F_REV=35899]="UNSIGNED_INT_10F_11F_11F_REV",n[n.UNSIGNED_INT_2_10_10_10_REV=33640]="UNSIGNED_INT_2_10_10_10_REV",n[n.UNSIGNED_INT_24_8=34042]="UNSIGNED_INT_24_8",n[n.UNSIGNED_INT_5_9_9_9_REV=35902]="UNSIGNED_INT_5_9_9_9_REV",n[n.BYTE=5120]="BYTE",n[n.SHORT=5122]="SHORT",n[n.INT=5124]="INT",n[n.FLOAT=5126]="FLOAT",n[n.FLOAT_32_UNSIGNED_INT_24_8_REV=36269]="FLOAT_32_UNSIGNED_INT_24_8_REV",n[n.HALF_FLOAT=36193]="HALF_FLOAT",n))(jt||{}),vt=(n=>(n[n.FLOAT=0]="FLOAT",n[n.INT=1]="INT",n[n.UINT=2]="UINT",n))(vt||{}),es=(n=>(n[n.NEAREST=0]="NEAREST",n[n.LINEAR=1]="LINEAR",n))(es||{}),S2=(n=>(n[n.CLAMP=33071]="CLAMP",n[n.REPEAT=10497]="REPEAT",n[n.MIRRORED_REPEAT=33648]="MIRRORED_REPEAT",n))(S2||{}),Nr=(n=>(n[n.OFF=0]="OFF",n[n.POW2=1]="POW2",n[n.ON=2]="ON",n[n.ON_MANUAL=3]="ON_MANUAL",n))(Nr||{}),Pr=(n=>(n[n.NPM=0]="NPM",n[n.UNPACK=1]="UNPACK",n[n.PMA=2]="PMA",n[n.NO_PREMULTIPLIED_ALPHA=0]="NO_PREMULTIPLIED_ALPHA",n[n.PREMULTIPLY_ON_UPLOAD=1]="PREMULTIPLY_ON_UPLOAD",n[n.PREMULTIPLIED_ALPHA=2]="PREMULTIPLIED_ALPHA",n))(Pr||{}),Ci=(n=>(n[n.NO=0]="NO",n[n.YES=1]="YES",n[n.AUTO=2]="AUTO",n[n.BLEND=0]="BLEND",n[n.CLEAR=1]="CLEAR",n[n.BLIT=2]="BLIT",n))(Ci||{}),_2=(n=>(n[n.AUTO=0]="AUTO",n[n.MANUAL=1]="MANUAL",n))(_2||{}),Yn=(n=>(n.LOW="lowp",n.MEDIUM="mediump",n.HIGH="highp",n))(Yn||{}),He=(n=>(n[n.NONE=0]="NONE",n[n.SCISSOR=1]="SCISSOR",n[n.STENCIL=2]="STENCIL",n[n.SPRITE=3]="SPRITE",n[n.COLOR=4]="COLOR",n))(He||{}),Ge=(n=>(n[n.NONE=0]="NONE",n[n.LOW=2]="LOW",n[n.MEDIUM=4]="MEDIUM",n[n.HIGH=8]="HIGH",n))(Ge||{}),_i=(n=>(n[n.ELEMENT_ARRAY_BUFFER=34963]="ELEMENT_ARRAY_BUFFER",n[n.ARRAY_BUFFER=34962]="ARRAY_BUFFER",n[n.UNIFORM_BUFFER=35345]="UNIFORM_BUFFER",n))(_i||{});const EM={createCanvas:(n,t)=>{const e=document.createElement("canvas");return e.width=n,e.height=t,e},getCanvasRenderingContext2D:()=>CanvasRenderingContext2D,getWebGLRenderingContext:()=>WebGLRenderingContext,getNavigator:()=>navigator,getBaseUrl:()=>document.baseURI??window.location.href,getFontFaceSet:()=>document.fonts,fetch:(n,t)=>fetch(n,t),parseXML:n=>new DOMParser().parseFromString(n,"text/xml")},Kt={ADAPTER:EM,RESOLUTION:1,CREATE_IMAGE_BITMAP:!1,ROUND_PIXELS:!1};var Cd=/iPhone/i,P5=/iPod/i,F5=/iPad/i,U5=/\biOS-universal(?:.+)Mac\b/i,Sd=/\bAndroid(?:.+)Mobile\b/i,k5=/Android/i,Aa=/(?:SD4930UR|\bSilk(?:.+)Mobile\b)/i,yu=/Silk/i,Zi=/Windows Phone/i,z5=/\bWindows(?:.+)ARM\b/i,V5=/BlackBerry/i,G5=/BB10/i,j5=/Opera Mini/i,H5=/\b(CriOS|Chrome)(?:.+)Mobile/i,q5=/Mobile(?:.+)Firefox\b/i,X5=function(n){return typeof n<"u"&&n.platform==="MacIntel"&&typeof n.maxTouchPoints=="number"&&n.maxTouchPoints>1&&typeof MSStream>"u"};function wM(n){return function(t){return t.test(n)}}function Y5(n){var t={userAgent:"",platform:"",maxTouchPoints:0};!n&&typeof navigator<"u"?t={userAgent:navigator.userAgent,platform:navigator.platform,maxTouchPoints:navigator.maxTouchPoints||0}:typeof n=="string"?t.userAgent=n:n&&n.userAgent&&(t={userAgent:n.userAgent,platform:n.platform,maxTouchPoints:n.maxTouchPoints||0});var e=t.userAgent,i=e.split("[FBAN");typeof i[1]<"u"&&(e=i[0]),i=e.split("Twitter"),typeof i[1]<"u"&&(e=i[0]);var a=wM(e),o={apple:{phone:a(Cd)&&!a(Zi),ipod:a(P5),tablet:!a(Cd)&&(a(F5)||X5(t))&&!a(Zi),universal:a(U5),device:(a(Cd)||a(P5)||a(F5)||a(U5)||X5(t))&&!a(Zi)},amazon:{phone:a(Aa),tablet:!a(Aa)&&a(yu),device:a(Aa)||a(yu)},android:{phone:!a(Zi)&&a(Aa)||!a(Zi)&&a(Sd),tablet:!a(Zi)&&!a(Aa)&&!a(Sd)&&(a(yu)||a(k5)),device:!a(Zi)&&(a(Aa)||a(yu)||a(Sd)||a(k5))||a(/\bokhttp\b/i)},windows:{phone:a(Zi),tablet:a(z5),device:a(Zi)||a(z5)},other:{blackberry:a(V5),blackberry10:a(G5),opera:a(j5),firefox:a(q5),chrome:a(H5),device:a(V5)||a(G5)||a(j5)||a(q5)||a(H5)},any:!1,phone:!1,tablet:!1};return o.any=o.apple.device||o.android.device||o.windows.device||o.other.device,o.phone=o.apple.phone||o.android.phone||o.windows.phone,o.tablet=o.apple.tablet||o.android.tablet||o.windows.tablet,o}const MM=Y5.default??Y5,ka=MM(globalThis.navigator);Kt.RETINA_PREFIX=/@([0-9\.]+)x/;Kt.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT=!1;var _d={exports:{}},$5;function RM(){return $5||($5=1,(function(n){var t=Object.prototype.hasOwnProperty,e="~";function i(){}Object.create&&(i.prototype=Object.create(null),new i().__proto__||(e=!1));function a(f,d,m){this.fn=f,this.context=d,this.once=m||!1}function o(f,d,m,g,v){if(typeof m!="function")throw new TypeError("The listener must be a function");var x=new a(m,g||f,v),b=e?e+d:d;return f._events[b]?f._events[b].fn?f._events[b]=[f._events[b],x]:f._events[b].push(x):(f._events[b]=x,f._eventsCount++),f}function c(f,d){--f._eventsCount===0?f._events=new i:delete f._events[d]}function u(){this._events=new i,this._eventsCount=0}u.prototype.eventNames=function(){var d=[],m,g;if(this._eventsCount===0)return d;for(g in m=this._events)t.call(m,g)&&d.push(e?g.slice(1):g);return Object.getOwnPropertySymbols?d.concat(Object.getOwnPropertySymbols(m)):d},u.prototype.listeners=function(d){var m=e?e+d:d,g=this._events[m];if(!g)return[];if(g.fn)return[g.fn];for(var v=0,x=g.length,b=new Array(x);v80*N){j=st=A[0],W=ct=A[1];for(var dt=N;dtst&&(st=ft),pt>ct&&(ct=pt);mt=Math.max(st-j,ct-W),mt=mt!==0?32767/mt:0}return i(M,z,N,j,W,mt,0),z}function t(A,B,N,H,V){var M,z;if(V===K(A,B,N,H)>0)for(M=B;M=B;M-=H)z=G(M,A[M],A[M+1],z);return z&&P(z,z.next)&&(J(z),z=z.next),z}function e(A,B){if(!A)return A;B||(B=A);var N=A,H;do if(H=!1,!N.steiner&&(P(N,N.next)||R(N.prev,N,N.next)===0)){if(J(N),N=B=N.prev,N===N.next)break;H=!0}else N=N.next;while(H||N!==B);return B}function i(A,B,N,H,V,M,z){if(A){!z&&M&&x(A,H,V,M);for(var j=A,W,st;A.prev!==A.next;){if(W=A.prev,st=A.next,M?o(A,H,V,M):a(A)){B.push(W.i/N|0),B.push(A.i/N|0),B.push(st.i/N|0),J(A),A=st.next,j=st.next;continue}if(A=st,A===j){z?z===1?(A=c(e(A),B,N),i(A,B,N,H,V,M,2)):z===2&&u(A,B,N,H,V,M):i(e(A),B,N,H,V,M,1);break}}}}function a(A){var B=A.prev,N=A,H=A.next;if(R(B,N,H)>=0)return!1;for(var V=B.x,M=N.x,z=H.x,j=B.y,W=N.y,st=H.y,ct=VM?V>z?V:z:M>z?M:z,mt=j>W?j>st?j:st:W>st?W:st,dt=H.next;dt!==B;){if(dt.x>=ct&&dt.x<=pt&&dt.y>=ft&&dt.y<=mt&&S(V,j,M,W,z,st,dt.x,dt.y)&&R(dt.prev,dt,dt.next)>=0)return!1;dt=dt.next}return!0}function o(A,B,N,H){var V=A.prev,M=A,z=A.next;if(R(V,M,z)>=0)return!1;for(var j=V.x,W=M.x,st=z.x,ct=V.y,ft=M.y,pt=z.y,mt=jW?j>st?j:st:W>st?W:st,bt=ct>ft?ct>pt?ct:pt:ft>pt?ft:pt,Lt=C(mt,dt,B,N,H),wt=C(St,bt,B,N,H),yt=A.prevZ,xt=A.nextZ;yt&&yt.z>=Lt&&xt&&xt.z<=wt;){if(yt.x>=mt&&yt.x<=St&&yt.y>=dt&&yt.y<=bt&&yt!==V&&yt!==z&&S(j,ct,W,ft,st,pt,yt.x,yt.y)&&R(yt.prev,yt,yt.next)>=0||(yt=yt.prevZ,xt.x>=mt&&xt.x<=St&&xt.y>=dt&&xt.y<=bt&&xt!==V&&xt!==z&&S(j,ct,W,ft,st,pt,xt.x,xt.y)&&R(xt.prev,xt,xt.next)>=0))return!1;xt=xt.nextZ}for(;yt&&yt.z>=Lt;){if(yt.x>=mt&&yt.x<=St&&yt.y>=dt&&yt.y<=bt&&yt!==V&&yt!==z&&S(j,ct,W,ft,st,pt,yt.x,yt.y)&&R(yt.prev,yt,yt.next)>=0)return!1;yt=yt.prevZ}for(;xt&&xt.z<=wt;){if(xt.x>=mt&&xt.x<=St&&xt.y>=dt&&xt.y<=bt&&xt!==V&&xt!==z&&S(j,ct,W,ft,st,pt,xt.x,xt.y)&&R(xt.prev,xt,xt.next)>=0)return!1;xt=xt.nextZ}return!0}function c(A,B,N){var H=A;do{var V=H.prev,M=H.next.next;!P(V,M)&&w(V,H,H.next,M)&&I(V,M)&&I(M,V)&&(B.push(V.i/N|0),B.push(H.i/N|0),B.push(M.i/N|0),J(H),J(H.next),H=A=M),H=H.next}while(H!==A);return e(H)}function u(A,B,N,H,V,M){var z=A;do{for(var j=z.next.next;j!==z.prev;){if(z.i!==j.i&&L(z,j)){var W=U(z,j);z=e(z,z.next),W=e(W,W.next),i(z,B,N,H,V,M,0),i(W,B,N,H,V,M,0);return}j=j.next}z=z.next}while(z!==A)}function f(A,B,N,H){var V=[],M,z,j,W,st;for(M=0,z=B.length;M=N.next.y&&N.next.y!==N.y){var j=N.x+(V-N.y)*(N.next.x-N.x)/(N.next.y-N.y);if(j<=H&&j>M&&(M=j,z=N.x=N.x&&N.x>=st&&H!==N.x&&S(Vz.x||N.x===z.x&&v(z,N)))&&(z=N,ft=pt)),N=N.next;while(N!==W);return z}function v(A,B){return R(A.prev,A,B.prev)<0&&R(B.next,A,A.next)<0}function x(A,B,N,H){var V=A;do V.z===0&&(V.z=C(V.x,V.y,B,N,H)),V.prevZ=V.prev,V.nextZ=V.next,V=V.next;while(V!==A);V.prevZ.nextZ=null,V.prevZ=null,b(V)}function b(A){var B,N,H,V,M,z,j,W,st=1;do{for(N=A,A=null,M=null,z=0;N;){for(z++,H=N,j=0,B=0;B0||W>0&&H;)j!==0&&(W===0||!H||N.z<=H.z)?(V=N,N=N.nextZ,j--):(V=H,H=H.nextZ,W--),M?M.nextZ=V:A=V,V.prevZ=M,M=V;N=H}M.nextZ=null,st*=2}while(z>1);return A}function C(A,B,N,H,V){return A=(A-N)*V|0,B=(B-H)*V|0,A=(A|A<<8)&16711935,A=(A|A<<4)&252645135,A=(A|A<<2)&858993459,A=(A|A<<1)&1431655765,B=(B|B<<8)&16711935,B=(B|B<<4)&252645135,B=(B|B<<2)&858993459,B=(B|B<<1)&1431655765,A|B<<1}function T(A){var B=A,N=A;do(B.x=(A-z)*(M-j)&&(A-z)*(H-j)>=(N-z)*(B-j)&&(N-z)*(M-j)>=(V-z)*(H-j)}function L(A,B){return A.next.i!==B.i&&A.prev.i!==B.i&&!O(A,B)&&(I(A,B)&&I(B,A)&&k(A,B)&&(R(A.prev,A,B.prev)||R(A,B.prev,B))||P(A,B)&&R(A.prev,A,A.next)>0&&R(B.prev,B,B.next)>0)}function R(A,B,N){return(B.y-A.y)*(N.x-B.x)-(B.x-A.x)*(N.y-B.y)}function P(A,B){return A.x===B.x&&A.y===B.y}function w(A,B,N,H){var V=E(R(A,B,N)),M=E(R(A,B,H)),z=E(R(N,H,A)),j=E(R(N,H,B));return!!(V!==M&&z!==j||V===0&&F(A,N,B)||M===0&&F(A,H,B)||z===0&&F(N,A,H)||j===0&&F(N,B,H))}function F(A,B,N){return B.x<=Math.max(A.x,N.x)&&B.x>=Math.min(A.x,N.x)&&B.y<=Math.max(A.y,N.y)&&B.y>=Math.min(A.y,N.y)}function E(A){return A>0?1:A<0?-1:0}function O(A,B){var N=A;do{if(N.i!==A.i&&N.next.i!==A.i&&N.i!==B.i&&N.next.i!==B.i&&w(N,N.next,A,B))return!0;N=N.next}while(N!==A);return!1}function I(A,B){return R(A.prev,A,A.next)<0?R(A,B,A.next)>=0&&R(A,A.prev,B)>=0:R(A,B,A.prev)<0||R(A,A.next,B)<0}function k(A,B){var N=A,H=!1,V=(A.x+B.x)/2,M=(A.y+B.y)/2;do N.y>M!=N.next.y>M&&N.next.y!==N.y&&V<(N.next.x-N.x)*(M-N.y)/(N.next.y-N.y)+N.x&&(H=!H),N=N.next;while(N!==A);return H}function U(A,B){var N=new tt(A.i,A.x,A.y),H=new tt(B.i,B.x,B.y),V=A.next,M=B.prev;return A.next=B,B.prev=A,N.next=V,V.prev=N,H.next=N,N.prev=H,M.next=H,H.prev=M,H}function G(A,B,N,H){var V=new tt(A,B,N);return H?(V.next=H.next,V.prev=H,H.next.prev=V,H.next=V):(V.prev=V,V.next=V),V}function J(A){A.next.prev=A.prev,A.prev.next=A.next,A.prevZ&&(A.prevZ.nextZ=A.nextZ),A.nextZ&&(A.nextZ.prevZ=A.prevZ)}function tt(A,B,N){this.i=A,this.x=B,this.y=N,this.prev=null,this.next=null,this.z=0,this.prevZ=null,this.nextZ=null,this.steiner=!1}n.deviation=function(A,B,N,H){var V=B&&B.length,M=V?B[0]*N:A.length,z=Math.abs(K(A,0,M,N));if(V)for(var j=0,W=B.length;j0&&(H+=A[V-1].length,N.holes.push(H))}return N},vu.exports}NM();var hr={},dl={exports:{}};var OM=dl.exports,Z5;function IM(){return Z5||(Z5=1,(function(n,t){(function(e){var i=t&&!t.nodeType&&t,a=n&&!n.nodeType&&n,o=typeof Zu=="object"&&Zu;(o.global===o||o.window===o||o.self===o)&&(e=o);var c,u=2147483647,f=36,d=1,m=26,g=38,v=700,x=72,b=128,C="-",T=/^xn--/,S=/[^\x20-\x7E]/,L=/[\x2E\u3002\uFF0E\uFF61]/g,R={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},P=f-d,w=Math.floor,F=String.fromCharCode,E;function O(V){throw new RangeError(R[V])}function I(V,M){for(var z=V.length,j=[];z--;)j[z]=M(V[z]);return j}function k(V,M){var z=V.split("@"),j="";z.length>1&&(j=z[0]+"@",V=z[1]),V=V.replace(L,".");var W=V.split("."),st=I(W,M).join(".");return j+st}function U(V){for(var M=[],z=0,j=V.length,W,st;z=55296&&W<=56319&&z65535&&(M-=65536,z+=F(M>>>10&1023|55296),M=56320|M&1023),z+=F(M),z}).join("")}function J(V){return V-48<10?V-22:V-65<26?V-65:V-97<26?V-97:f}function tt(V,M){return V+22+75*(V<26)-((M!=0)<<5)}function K(V,M,z){var j=0;for(V=z?w(V/v):V>>1,V+=w(V/M);V>P*m>>1;j+=f)V=w(V/P);return w(j+(P+1)*V/(V+g))}function A(V){var M=[],z=V.length,j,W=0,st=b,ct=x,ft,pt,mt,dt,St,bt,Lt,wt,yt;for(ft=V.lastIndexOf(C),ft<0&&(ft=0),pt=0;pt=128&&O("not-basic"),M.push(V.charCodeAt(pt));for(mt=ft>0?ft+1:0;mt=z&&O("invalid-input"),Lt=J(V.charCodeAt(mt++)),(Lt>=f||Lt>w((u-W)/St))&&O("overflow"),W+=Lt*St,wt=bt<=ct?d:bt>=ct+m?m:bt-ct,!(Ltw(u/yt)&&O("overflow"),St*=yt;j=M.length+1,ct=K(W-dt,j,dt==0),w(W/j)>u-st&&O("overflow"),st+=w(W/j),W%=j,M.splice(W++,0,st)}return G(M)}function B(V){var M,z,j,W,st,ct,ft,pt,mt,dt,St,bt=[],Lt,wt,yt,xt;for(V=U(V),Lt=V.length,M=b,z=0,st=x,ct=0;ct=M&&Stw((u-z)/wt)&&O("overflow"),z+=(ft-M)*wt,M=ft,ct=0;ctu&&O("overflow"),St==M){for(pt=z,mt=f;dt=mt<=st?d:mt>=st+m?m:mt-st,!(pt-1e3&&Q<1e3||F.call(/e/,rt))return rt;var Qt=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if(typeof Q=="number"){var fe=Q<0?-k(-Q):k(Q);if(fe!==Q){var oe=String(fe),It=L.call(rt,oe.length+1);return R.call(oe,Qt,"$&_")+"."+R.call(R.call(It,/([0-9]{3})/g,"$&_"),/_$/,"")}}return R.call(rt,Qt,"$&_")}var H=PM,V=H.custom,M=wt(V)?V:null,z={__proto__:null,double:'"',single:"'"},j={__proto__:null,double:/(["\\])/g,single:/(['\\])/g};Ed=function Q(rt,Qt,fe,oe){var It=Qt||{};if(ie(It,"quoteStyle")&&!ie(z,It.quoteStyle))throw new TypeError('option "quoteStyle" must be "single" or "double"');if(ie(It,"maxStringLength")&&(typeof It.maxStringLength=="number"?It.maxStringLength<0&&It.maxStringLength!==1/0:It.maxStringLength!==null))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var An=ie(It,"customInspect")?It.customInspect:!0;if(typeof An!="boolean"&&An!=="symbol")throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(ie(It,"indent")&&It.indent!==null&&It.indent!==" "&&!(parseInt(It.indent,10)===It.indent&&It.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(ie(It,"numericSeparator")&&typeof It.numericSeparator!="boolean")throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var cn=It.numericSeparator;if(typeof rt>"u")return"undefined";if(rt===null)return"null";if(typeof rt=="boolean")return rt?"true":"false";if(typeof rt=="string")return io(rt,It);if(typeof rt=="number"){if(rt===0)return 1/0/rt>0?"0":"-0";var Xe=String(rt);return cn?N(rt,Xe):Xe}if(typeof rt=="bigint"){var pn=String(rt)+"n";return cn?N(rt,pn):pn}var so=typeof It.depth>"u"?5:It.depth;if(typeof fe>"u"&&(fe=0),fe>=so&&so>0&&typeof rt=="object")return ft(rt)?"[Array]":"[Object]";var Ri=qs(It,fe);if(typeof oe>"u")oe=[];else if(Dn(oe,rt)>=0)return"[Circular]";function un(Li,di,lo){if(di&&(oe=I.call(oe),oe.push(di)),lo){var Ni={depth:It.depth};return ie(It,"quoteStyle")&&(Ni.quoteStyle=It.quoteStyle),Q(Li,Ni,fe+1,oe)}return Q(Li,It,fe+1,oe)}if(typeof rt=="function"&&!mt(rt)){var as=Sn(rt),Ql=_n(rt,un);return"[Function"+(as?": "+as:" (anonymous)")+"]"+(Ql.length>0?" { "+O.call(Ql,", ")+" }":"")}if(wt(rt)){var ro=tt?R.call(String(rt),/^(Symbol\(.*\))_[^)]*$/,"$1"):J.call(rt);return typeof rt=="object"&&!tt?Hs(ro):ro}if(Zl(rt)){for(var os="<"+w.call(String(rt.nodeName)),zr=rt.attributes||[],ls=0;ls",os}if(ft(rt)){if(rt.length===0)return"[]";var Xs=_n(rt,un);return Ri&&!Uh(Xs)?"["+Ze(Xs,Ri)+"]":"[ "+O.call(Xs,", ")+" ]"}if(dt(rt)){var Vr=_n(rt,un);return!("cause"in Error.prototype)&&"cause"in rt&&!A.call(rt,"cause")?"{ ["+String(rt)+"] "+O.call(E.call("[cause]: "+un(rt.cause),Vr),", ")+" }":Vr.length===0?"["+String(rt)+"]":"{ ["+String(rt)+"] "+O.call(Vr,", ")+" }"}if(typeof rt=="object"&&An){if(M&&typeof rt[M]=="function"&&H)return H(rt,{depth:so-fe});if(An!=="symbol"&&typeof rt.inspect=="function")return rt.inspect()}if(ln(rt)){var ao=[];return i&&i.call(rt,function(Li,di){ao.push(un(di,rt,!0)+" => "+un(Li,rt))}),Kl("Map",e.call(rt),ao,Ri)}if(Be(rt)){var Jn=[];return u&&u.call(rt,function(Li){Jn.push(un(Li,rt))}),Kl("Set",c.call(rt),Jn,Ri)}if(ss(rt))return kr("WeakMap");if(Fh(rt))return kr("WeakSet");if(rs(rt))return kr("WeakRef");if(bt(rt))return Hs(un(Number(rt)));if(yt(rt))return Hs(un(U.call(rt)));if(Lt(rt))return Hs(b.call(rt));if(St(rt))return Hs(un(String(rt)));if(typeof window<"u"&&rt===window)return"{ [object Window] }";if(typeof globalThis<"u"&&rt===globalThis||typeof Zu<"u"&&rt===Zu)return"{ [object globalThis] }";if(!pt(rt)&&!mt(rt)){var Le=_n(rt,un),Ke=B?B(rt)===Object.prototype:rt instanceof Object||rt.constructor===Object,fi=rt instanceof Object?"":"null prototype",Gr=!Ke&&K&&Object(rt)===rt&&K in rt?L.call(De(rt),8,-1):fi?"Object":"",kh=Ke||typeof rt.constructor!="function"?"":rt.constructor.name?rt.constructor.name+" ":"",oo=kh+(Gr||fi?"["+O.call(E.call([],Gr||[],fi||[]),": ")+"] ":"");return Le.length===0?oo+"{}":Ri?oo+"{"+Ze(Le,Ri)+"}":oo+"{ "+O.call(Le,", ")+" }"}return String(rt)};function W(Q,rt,Qt){var fe=Qt.quoteStyle||rt,oe=z[fe];return oe+Q+oe}function st(Q){return R.call(String(Q),/"/g,""")}function ct(Q){return!K||!(typeof Q=="object"&&(K in Q||typeof Q[K]<"u"))}function ft(Q){return De(Q)==="[object Array]"&&ct(Q)}function pt(Q){return De(Q)==="[object Date]"&&ct(Q)}function mt(Q){return De(Q)==="[object RegExp]"&&ct(Q)}function dt(Q){return De(Q)==="[object Error]"&&ct(Q)}function St(Q){return De(Q)==="[object String]"&&ct(Q)}function bt(Q){return De(Q)==="[object Number]"&&ct(Q)}function Lt(Q){return De(Q)==="[object Boolean]"&&ct(Q)}function wt(Q){if(tt)return Q&&typeof Q=="object"&&Q instanceof Symbol;if(typeof Q=="symbol")return!0;if(!Q||typeof Q!="object"||!J)return!1;try{return J.call(Q),!0}catch{}return!1}function yt(Q){if(!Q||typeof Q!="object"||!U)return!1;try{return U.call(Q),!0}catch{}return!1}var xt=Object.prototype.hasOwnProperty||function(Q){return Q in this};function ie(Q,rt){return xt.call(Q,rt)}function De(Q){return C.call(Q)}function Sn(Q){if(Q.name)return Q.name;var rt=S.call(T.call(Q),/^function\s*([\w$]+)/);return rt?rt[1]:null}function Dn(Q,rt){if(Q.indexOf)return Q.indexOf(rt);for(var Qt=0,fe=Q.length;Qtrt.maxStringLength){var Qt=Q.length-rt.maxStringLength,fe="... "+Qt+" more character"+(Qt>1?"s":"");return io(L.call(Q,0,rt.maxStringLength),rt)+fe}var oe=j[rt.quoteStyle||"single"];oe.lastIndex=0;var It=R.call(R.call(Q,oe,"\\$1"),/[\x00-\x1f]/g,Ur);return W(It,"single",rt)}function Ur(Q){var rt=Q.charCodeAt(0),Qt={8:"b",9:"t",10:"n",12:"f",13:"r"}[rt];return Qt?"\\"+Qt:"\\x"+(rt<16?"0":"")+P.call(rt.toString(16))}function Hs(Q){return"Object("+Q+")"}function kr(Q){return Q+" { ? }"}function Kl(Q,rt,Qt,fe){var oe=fe?Ze(Qt,fe):O.call(Qt,", ");return Q+" ("+rt+") {"+oe+"}"}function Uh(Q){for(var rt=0;rt=0)return!1;return!0}function qs(Q,rt){var Qt;if(Q.indent===" ")Qt=" ";else if(typeof Q.indent=="number"&&Q.indent>0)Qt=O.call(Array(Q.indent+1)," ");else return null;return{base:Qt,prev:O.call(Array(rt+1),Qt)}}function Ze(Q,rt){if(Q.length===0)return"";var Qt=` +`+rt.prev+rt.base;return Qt+O.call(Q,","+Qt)+` +`+rt.prev}function _n(Q,rt){var Qt=ft(Q),fe=[];if(Qt){fe.length=Q.length;for(var oe=0;oe"u"||!E?n:E(Uint8Array),tt={__proto__:null,"%AggregateError%":typeof AggregateError>"u"?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":typeof ArrayBuffer>"u"?n:ArrayBuffer,"%ArrayIteratorPrototype%":F&&E?E([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":G,"%AsyncGenerator%":G,"%AsyncGeneratorFunction%":G,"%AsyncIteratorPrototype%":G,"%Atomics%":typeof Atomics>"u"?n:Atomics,"%BigInt%":typeof BigInt>"u"?n:BigInt,"%BigInt64Array%":typeof BigInt64Array>"u"?n:BigInt64Array,"%BigUint64Array%":typeof BigUint64Array>"u"?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":typeof DataView>"u"?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":e,"%eval%":eval,"%EvalError%":i,"%Float16Array%":typeof Float16Array>"u"?n:Float16Array,"%Float32Array%":typeof Float32Array>"u"?n:Float32Array,"%Float64Array%":typeof Float64Array>"u"?n:Float64Array,"%FinalizationRegistry%":typeof FinalizationRegistry>"u"?n:FinalizationRegistry,"%Function%":T,"%GeneratorFunction%":G,"%Int8Array%":typeof Int8Array>"u"?n:Int8Array,"%Int16Array%":typeof Int16Array>"u"?n:Int16Array,"%Int32Array%":typeof Int32Array>"u"?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":F&&E?E(E([][Symbol.iterator]())):n,"%JSON%":typeof JSON=="object"?JSON:n,"%Map%":typeof Map>"u"?n:Map,"%MapIteratorPrototype%":typeof Map>"u"||!F||!E?n:E(new Map()[Symbol.iterator]()),"%Math%":Math,"%Number%":Number,"%Object%":t,"%Object.getOwnPropertyDescriptor%":L,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":typeof Promise>"u"?n:Promise,"%Proxy%":typeof Proxy>"u"?n:Proxy,"%RangeError%":a,"%ReferenceError%":o,"%Reflect%":typeof Reflect>"u"?n:Reflect,"%RegExp%":RegExp,"%Set%":typeof Set>"u"?n:Set,"%SetIteratorPrototype%":typeof Set>"u"||!F||!E?n:E(new Set()[Symbol.iterator]()),"%SharedArrayBuffer%":typeof SharedArrayBuffer>"u"?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":F&&E?E(""[Symbol.iterator]()):n,"%Symbol%":F?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":w,"%TypedArray%":J,"%TypeError%":u,"%Uint8Array%":typeof Uint8Array>"u"?n:Uint8Array,"%Uint8ClampedArray%":typeof Uint8ClampedArray>"u"?n:Uint8ClampedArray,"%Uint16Array%":typeof Uint16Array>"u"?n:Uint16Array,"%Uint32Array%":typeof Uint32Array>"u"?n:Uint32Array,"%URIError%":f,"%WeakMap%":typeof WeakMap>"u"?n:WeakMap,"%WeakRef%":typeof WeakRef>"u"?n:WeakRef,"%WeakSet%":typeof WeakSet>"u"?n:WeakSet,"%Function.prototype.call%":U,"%Function.prototype.apply%":k,"%Object.defineProperty%":R,"%Object.getPrototypeOf%":O,"%Math.abs%":d,"%Math.floor%":m,"%Math.max%":g,"%Math.min%":v,"%Math.pow%":x,"%Math.round%":b,"%Math.sign%":C,"%Reflect.getPrototypeOf%":I};if(E)try{null.error}catch(mt){var K=E(E(mt));tt["%Error.prototype%"]=K}var A=function mt(dt){var St;if(dt==="%AsyncFunction%")St=S("async function () {}");else if(dt==="%GeneratorFunction%")St=S("function* () {}");else if(dt==="%AsyncGeneratorFunction%")St=S("async function* () {}");else if(dt==="%AsyncGenerator%"){var bt=mt("%AsyncGeneratorFunction%");bt&&(St=bt.prototype)}else if(dt==="%AsyncIteratorPrototype%"){var Lt=mt("%AsyncGenerator%");Lt&&E&&(St=E(Lt.prototype))}return tt[dt]=St,St},B={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},N=Mh(),H=aR(),V=N.call(U,Array.prototype.concat),M=N.call(k,Array.prototype.splice),z=N.call(U,String.prototype.replace),j=N.call(U,String.prototype.slice),W=N.call(U,RegExp.prototype.exec),st=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,ct=/\\(\\)?/g,ft=function(dt){var St=j(dt,0,1),bt=j(dt,-1);if(St==="%"&&bt!=="%")throw new c("invalid intrinsic syntax, expected closing `%`");if(bt==="%"&&St!=="%")throw new c("invalid intrinsic syntax, expected opening `%`");var Lt=[];return z(dt,st,function(wt,yt,xt,ie){Lt[Lt.length]=xt?z(ie,ct,"$1"):yt||wt}),Lt},pt=function(dt,St){var bt=dt,Lt;if(H(B,bt)&&(Lt=B[bt],bt="%"+Lt[0]+"%"),H(tt,bt)){var wt=tt[bt];if(wt===G&&(wt=A(bt)),typeof wt>"u"&&!St)throw new u("intrinsic "+dt+" exists, but is not available. Please file an issue!");return{alias:Lt,name:bt,value:wt}}throw new c("intrinsic "+dt+" does not exist!")};return a0=function(dt,St){if(typeof dt!="string"||dt.length===0)throw new u("intrinsic name must be a non-empty string");if(arguments.length>1&&typeof St!="boolean")throw new u('"allowMissing" argument must be a boolean');if(W(/^%?[^%]*%?$/,dt)===null)throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var bt=ft(dt),Lt=bt.length>0?bt[0]:"",wt=pt("%"+Lt+"%",St),yt=wt.name,xt=wt.value,ie=!1,De=wt.alias;De&&(Lt=De[0],M(bt,V([0,1],De)));for(var Sn=1,Dn=!0;Sn=bt.length){var Be=L(xt,ln);Dn=!!Be,Dn&&"get"in Be&&!("originalValue"in Be.get)?xt=Be.get:xt=xt[ln]}else Dn=H(xt,ln),xt=xt[ln];Dn&&!ie&&(tt[yt]=xt)}}return xt},a0}var o0,I6;function J9(){if(I6)return o0;I6=1;var n=E2(),t=Q9(),e=t([n("%String.prototype.indexOf%")]);return o0=function(a,o){var c=n(a,!!o);return typeof c=="function"&&e(a,".prototype.")>-1?t([c]):c},o0}var l0,D6;function tv(){if(D6)return l0;D6=1;var n=E2(),t=J9(),e=wh(),i=to(),a=n("%Map%",!0),o=t("Map.prototype.get",!0),c=t("Map.prototype.set",!0),u=t("Map.prototype.has",!0),f=t("Map.prototype.delete",!0),d=t("Map.prototype.size",!0);return l0=!!a&&function(){var g,v={assert:function(x){if(!v.has(x))throw new i("Side channel does not contain "+e(x))},delete:function(x){if(g){var b=f(g,x);return d(g)===0&&(g=void 0),b}return!1},get:function(x){if(g)return o(g,x)},has:function(x){return g?u(g,x):!1},set:function(x,b){g||(g=new a),c(g,x,b)}};return v},l0}var c0,B6;function oR(){if(B6)return c0;B6=1;var n=E2(),t=J9(),e=wh(),i=tv(),a=to(),o=n("%WeakMap%",!0),c=t("WeakMap.prototype.get",!0),u=t("WeakMap.prototype.set",!0),f=t("WeakMap.prototype.has",!0),d=t("WeakMap.prototype.delete",!0);return c0=o?function(){var g,v,x={assert:function(b){if(!x.has(b))throw new a("Side channel does not contain "+e(b))},delete:function(b){if(o&&b&&(typeof b=="object"||typeof b=="function")){if(g)return d(g,b)}else if(i&&v)return v.delete(b);return!1},get:function(b){return o&&b&&(typeof b=="object"||typeof b=="function")&&g?c(g,b):v&&v.get(b)},has:function(b){return o&&b&&(typeof b=="object"||typeof b=="function")&&g?f(g,b):!!v&&v.has(b)},set:function(b,C){o&&b&&(typeof b=="object"||typeof b=="function")?(g||(g=new o),u(g,b,C)):i&&(v||(v=i()),v.set(b,C))}};return x}:i,c0}var u0,P6;function ev(){if(P6)return u0;P6=1;var n=to(),t=wh(),e=FM(),i=tv(),a=oR(),o=a||i||e;return u0=function(){var u,f={assert:function(d){if(!f.has(d)){var m=d&&Object(d)===d?"the given object key":t(d);throw new n("Side channel does not contain "+m)}},delete:function(d){return!!u&&u.delete(d)},get:function(d){return u&&u.get(d)},has:function(d){return!!u&&u.has(d)},set:function(d,m){u||(u=o()),u.set(d,m)}};return f},u0}var h0,F6;function w2(){if(F6)return h0;F6=1;var n=String.prototype.replace,t=/%20/g,e={RFC1738:"RFC1738",RFC3986:"RFC3986"};return h0={default:e.RFC3986,formatters:{RFC1738:function(i){return n.call(i,t,"+")},RFC3986:function(i){return String(i)}},RFC1738:e.RFC1738,RFC3986:e.RFC3986},h0}var f0,U6;function nv(){if(U6)return f0;U6=1;var n=w2(),t=ev(),e=$9(),i=Object.prototype.hasOwnProperty,a=Array.isArray,o=t(),c=function(I,k){return o.set(I,k),I},u=function(I){return o.has(I)},f=function(I){return o.get(I)},d=function(I,k){o.set(I,k)},m=(function(){for(var O=[],I=0;I<256;++I)O[O.length]="%"+((I<16?"0":"")+I.toString(16)).toUpperCase();return O})(),g=function(I){for(;I.length>1;){var k=I.pop(),U=k.obj[k.prop];if(a(U)){for(var G=[],J=0;J=U.arrayLimit){if(U.throwOnLimitExceeded)throw new RangeError("Array limit exceeded. Only "+U.arrayLimit+" element"+(U.arrayLimit===1?"":"s")+" allowed in an array.");return c(v(I.concat(k),U),G)}I[G]=k}else if(I&&typeof I=="object")if(u(I)){var J=f(I)+1;I[J]=k,d(I,J)}else{if(U&&U.strictMerge)return[I,k];(U&&(U.plainObjects||U.allowPrototypes)||!i.call(Object.prototype,k))&&(I[k]=!0)}else return[I,k];return I}if(!I||typeof I!="object"){if(u(k)){for(var tt=Object.keys(k),K=U&&U.plainObjects?{__proto__:null,0:I}:{0:I},A=0;AU.arrayLimit){if(U.throwOnLimitExceeded)throw new RangeError("Array limit exceeded. Only "+U.arrayLimit+" element"+(U.arrayLimit===1?"":"s")+" allowed in an array.");return c(v(N,U),N.length-1)}return N}var H=I;if(a(I)&&!a(k)&&(H=v(I,U)),a(I)&&a(k)){if(k.forEach(function(V,M){if(i.call(I,M)){var z=I[M];z&&typeof z=="object"&&V&&typeof V=="object"?I[M]=O(z,V,U):I[I.length]=V}else I[M]=V}),U&&typeof U.arrayLimit=="number"&&I.length>U.arrayLimit){if(U.throwOnLimitExceeded)throw new RangeError("Array limit exceeded. Only "+U.arrayLimit+" element"+(U.arrayLimit===1?"":"s")+" allowed in an array.");return c(v(I,U),I.length-1)}return I}return Object.keys(k).reduce(function(V,M){var z=k[M];if(i.call(V,M)?x(V,M,O(V[M],z,U)):x(V,M,z),u(k)&&!u(V)&&c(V,f(k)),u(V)){var j=parseInt(M,10);String(j)===M&&j>=0&&j>f(V)&&d(V,j)}return V},H)},C=function(I,k){return Object.keys(k).reduce(function(U,G){return x(U,G,k[G]),U},I)},T=function(O,I,k){var U=O.replace(/\+/g," ");if(k==="iso-8859-1")return U.replace(/%[0-9a-f]{2}/gi,unescape);try{return decodeURIComponent(U)}catch{return U}},S=1024,L=function(I,k,U,G,J){if(I.length===0)return I;var tt=I;if(typeof I=="symbol"?tt=Symbol.prototype.toString.call(I):typeof I!="string"&&(tt=String(I)),U==="iso-8859-1")return escape(tt).replace(/%u[0-9a-f]{4}/gi,function(z){return"%26%23"+parseInt(z.slice(2),16)+"%3B"});for(var K="",A=0;A=S?tt.slice(A,A+S):tt;if(A+S=55296&&N<=56319&&(B=B.slice(0,-1),A-=1)}for(var H=[],V=0;V=48&&M<=57||M>=65&&M<=90||M>=97&&M<=122||J===n.RFC1738&&(M===40||M===41)){H[H.length]=B.charAt(V);continue}if(M<128){H[H.length]=m[M];continue}if(M<2048){H[H.length]=m[192|M>>6]+m[128|M&63];continue}if(M<55296||M>=57344){H[H.length]=m[224|M>>12]+m[128|M>>6&63]+m[128|M&63];continue}V+=1,M=65536+((M&1023)<<10|B.charCodeAt(V)&1023),H[H.length]=m[240|M>>18]+m[128|M>>12&63]+m[128|M>>6&63]+m[128|M&63]}K+=H.join("")}return K},R=function(I){for(var k=[{obj:{o:I},prop:"o"}],U=t(),G=0;GU){if(J)throw new RangeError("Array limit exceeded. Only "+U+" element"+(U===1?"":"s")+" allowed in an array.");return c(v(K,{plainObjects:G}),K.length-1)}return K},E=function(I,k){if(a(I)){for(var U=[],G=0;G"u"&&(V=0)}if(typeof I=="function"?N=I(S,N):N instanceof Date?N=G(N):L==="comma"&&o(N)&&(N=t.maybeMap(N,function(yt){return yt instanceof Date?G(yt):yt})),N===null){if(w)return tt(O&&!K?O(S,m.encoder,A,"key",J):S);N=""}if(g(N)||t.isBuffer(N)){if(O){var j=K?S:O(S,m.encoder,A,"key",J);return[tt(j)+"="+tt(O(N,m.encoder,A,"value",J))]}return[tt(S)+"="+tt(String(N))]}var W=[];if(typeof N>"u")return W;var st;if(L==="comma"&&o(N))K&&O&&(N=t.maybeMap(N,function(yt){return yt==null?yt:O(yt)})),st=[{value:N.length>0?N.join(",")||null:void 0}];else if(o(I))st=I;else{var ct=Object.keys(N);st=k?ct.sort(k):ct}var ft=E?String(S).replace(/\./g,"%2E"):String(S),pt=R&&o(N)&&N.length===1?ft+"[]":ft;if(P&&o(N)&&N.length===0)return pt+"[]";for(var mt=0;mt"u"?T.encodeDotInKeys===!0?!0:m.allowDots:!!T.allowDots;return{addQueryPrefix:typeof T.addQueryPrefix=="boolean"?T.addQueryPrefix:m.addQueryPrefix,allowDots:F,allowEmptyArrays:typeof T.allowEmptyArrays=="boolean"?!!T.allowEmptyArrays:m.allowEmptyArrays,arrayFormat:w,charset:S,charsetSentinel:typeof T.charsetSentinel=="boolean"?T.charsetSentinel:m.charsetSentinel,commaRoundTrip:!!T.commaRoundTrip,delimiter:typeof T.delimiter>"u"?m.delimiter:T.delimiter,encode:typeof T.encode=="boolean"?T.encode:m.encode,encodeDotInKeys:typeof T.encodeDotInKeys=="boolean"?T.encodeDotInKeys:m.encodeDotInKeys,encoder:typeof T.encoder=="function"?T.encoder:m.encoder,encodeValuesOnly:typeof T.encodeValuesOnly=="boolean"?T.encodeValuesOnly:m.encodeValuesOnly,filter:P,format:L,formatter:R,serializeDate:typeof T.serializeDate=="function"?T.serializeDate:m.serializeDate,skipNulls:typeof T.skipNulls=="boolean"?T.skipNulls:m.skipNulls,sort:typeof T.sort=="function"?T.sort:null,strictNullHandling:typeof T.strictNullHandling=="boolean"?T.strictNullHandling:m.strictNullHandling}};return d0=function(C,T){var S=C,L=b(T),R,P;typeof L.filter=="function"?(P=L.filter,S=P("",S)):o(L.filter)&&(P=L.filter,R=P);var w=[];if(typeof S!="object"||S===null)return"";var F=a[L.arrayFormat],E=F==="comma"&&L.commaRoundTrip;R||(R=Object.keys(S)),L.sort&&R.sort(L.sort);for(var O=n(),I=0;I"u"||k===null)){var U=S[k];L.skipNulls&&U===null||u(w,x(U,k,F,E,L.allowEmptyArrays,L.strictNullHandling,L.skipNulls,L.encodeDotInKeys,L.encode?L.encoder:null,L.filter,L.sort,L.allowDots,L.serializeDate,L.format,L.formatter,L.encodeValuesOnly,L.charset,O))}}var G=w.join(L.delimiter),J=L.addQueryPrefix===!0?"?":"";return L.charsetSentinel&&(L.charset==="iso-8859-1"?J+="utf8=%26%2310003%3B"+L.delimiter:J+="utf8=%E2%9C%93"+L.delimiter),G.length>0?J+G:""},d0}var m0,z6;function cR(){if(z6)return m0;z6=1;var n=nv(),t=Object.prototype.hasOwnProperty,e=Array.isArray,i={allowDots:!1,allowEmptyArrays:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decodeDotInKeys:!1,decoder:n.decode,delimiter:"&",depth:5,duplicates:"combine",ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictDepth:!1,strictMerge:!0,strictNullHandling:!1,throwOnLimitExceeded:!1},a=function(x){return x.replace(/&#(\d+);/g,function(b,C){return String.fromCharCode(parseInt(C,10))})},o=function(x,b,C,T){if(x&&typeof x=="string"&&b.comma&&x.indexOf(",")>-1){if(T&&b.throwOnLimitExceeded)for(var S=0,L=x.indexOf(",");L>-1;){if(S+=1,S>=b.arrayLimit)throw new RangeError("Array limit exceeded. Only "+b.arrayLimit+" element"+(b.arrayLimit===1?"":"s")+" allowed in an array.");L=x.indexOf(",",L+1)}return x.split(",")}if(b.throwOnLimitExceeded&&C>=b.arrayLimit)throw new RangeError("Array limit exceeded. Only "+b.arrayLimit+" element"+(b.arrayLimit===1?"":"s")+" allowed in an array.");return x},c="utf8=%26%2310003%3B",u="utf8=%E2%9C%93",f=function(b,C){var T={__proto__:null},S=C.ignoreQueryPrefix?b.replace(/^\?/,""):b;S=S.replace(/%5B/gi,"[").replace(/%5D/gi,"]");var L=C.parameterLimit===1/0?void 0:C.parameterLimit,R=S.split(C.delimiter,C.throwOnLimitExceeded&&typeof L<"u"?L+1:L);if(C.throwOnLimitExceeded&&typeof L<"u"&&R.length>L)throw new RangeError("Parameter limit exceeded. Only "+L+" parameter"+(L===1?"":"s")+" allowed.");var P=-1,w,F=C.charset;if(C.charsetSentinel)for(w=0;w-1&&(U=e(U)?[U]:U),C.comma&&e(U)&&U.length>C.arrayLimit&&(U=n.combine([],U,C.arrayLimit,C.plainObjects,C.throwOnLimitExceeded)),k!==null){var G=t.call(T,k);G&&(C.duplicates==="combine"||E.indexOf("[]=")>-1)?T[k]=n.combine(T[k],U,C.arrayLimit,C.plainObjects,C.throwOnLimitExceeded):(!G||C.duplicates==="last")&&(T[k]=U)}}return T},d=function(x,b,C,T){var S=0;if(x.length>0&&x[x.length-1]==="[]"){var L=x.slice(0,-1).join("");S=Array.isArray(b)&&b[L]?b[L].length:0}for(var R=T?b:o(b,C,S),P=x.length-1;P>=0;--P){var w,F=x[P];if(F==="[]"&&C.parseArrays)n.isOverflow(R)?w=R:w=C.allowEmptyArrays&&(R===""||C.strictNullHandling&&R===null)?[]:n.combine([],R,C.arrayLimit,C.plainObjects,C.throwOnLimitExceeded);else{w=C.plainObjects?{__proto__:null}:{};var E=F.charAt(0)==="["&&F.charAt(F.length-1)==="]"?F.slice(1,-1):F,O=C.decodeDotInKeys?E.replace(/%2E/g,"."):E,I=parseInt(O,10),k=!isNaN(I)&&F!==O&&String(I)===O&&I>=0&&C.parseArrays;if(!C.parseArrays&&O==="")w={0:R};else if(k&&I=0?T.slice(0,L):T;if(R){if(!C.plainObjects&&t.call(Object.prototype,R)&&!C.allowPrototypes)return;S[S.length]=R}for(var P=T.length,w=L,F=0;w>=0&&F=0){if(C.strictDepth===!0)throw new RangeError("Input depth exceeded depth option of "+C.depth+" and strictDepth is true");S[S.length]="["+T.slice(w)+"]"}return S},g=function(b,C,T,S){if(b){var L=m(b,T);if(L)return d(L,C,T,S)}},v=function(b){if(!b)return i;if(typeof b.allowEmptyArrays<"u"&&typeof b.allowEmptyArrays!="boolean")throw new TypeError("`allowEmptyArrays` option can only be `true` or `false`, when provided");if(typeof b.decodeDotInKeys<"u"&&typeof b.decodeDotInKeys!="boolean")throw new TypeError("`decodeDotInKeys` option can only be `true` or `false`, when provided");if(b.decoder!==null&&typeof b.decoder<"u"&&typeof b.decoder!="function")throw new TypeError("Decoder has to be a function.");if(typeof b.charset<"u"&&b.charset!=="utf-8"&&b.charset!=="iso-8859-1")throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");if(typeof b.throwOnLimitExceeded<"u"&&typeof b.throwOnLimitExceeded!="boolean")throw new TypeError("`throwOnLimitExceeded` option must be a boolean");var C=typeof b.charset>"u"?i.charset:b.charset,T=typeof b.duplicates>"u"?i.duplicates:b.duplicates;if(T!=="combine"&&T!=="first"&&T!=="last")throw new TypeError("The duplicates option must be either combine, first, or last");var S=typeof b.allowDots>"u"?b.decodeDotInKeys===!0?!0:i.allowDots:!!b.allowDots;return{allowDots:S,allowEmptyArrays:typeof b.allowEmptyArrays=="boolean"?!!b.allowEmptyArrays:i.allowEmptyArrays,allowPrototypes:typeof b.allowPrototypes=="boolean"?b.allowPrototypes:i.allowPrototypes,allowSparse:typeof b.allowSparse=="boolean"?b.allowSparse:i.allowSparse,arrayLimit:typeof b.arrayLimit=="number"?b.arrayLimit:i.arrayLimit,charset:C,charsetSentinel:typeof b.charsetSentinel=="boolean"?b.charsetSentinel:i.charsetSentinel,comma:typeof b.comma=="boolean"?b.comma:i.comma,decodeDotInKeys:typeof b.decodeDotInKeys=="boolean"?b.decodeDotInKeys:i.decodeDotInKeys,decoder:typeof b.decoder=="function"?b.decoder:i.decoder,delimiter:typeof b.delimiter=="string"||n.isRegExp(b.delimiter)?b.delimiter:i.delimiter,depth:typeof b.depth=="number"||b.depth===!1?+b.depth:i.depth,duplicates:T,ignoreQueryPrefix:b.ignoreQueryPrefix===!0,interpretNumericEntities:typeof b.interpretNumericEntities=="boolean"?b.interpretNumericEntities:i.interpretNumericEntities,parameterLimit:typeof b.parameterLimit=="number"?b.parameterLimit:i.parameterLimit,parseArrays:b.parseArrays!==!1,plainObjects:typeof b.plainObjects=="boolean"?b.plainObjects:i.plainObjects,strictDepth:typeof b.strictDepth=="boolean"?!!b.strictDepth:i.strictDepth,strictMerge:typeof b.strictMerge=="boolean"?!!b.strictMerge:i.strictMerge,strictNullHandling:typeof b.strictNullHandling=="boolean"?b.strictNullHandling:i.strictNullHandling,throwOnLimitExceeded:typeof b.throwOnLimitExceeded=="boolean"?b.throwOnLimitExceeded:!1}};return m0=function(x,b){var C=v(b);if(x===""||x===null||typeof x>"u")return C.plainObjects?{__proto__:null}:{};for(var T=typeof x=="string"?f(x,C):x,S=C.plainObjects?{__proto__:null}:{},L=Object.keys(T),R=0;R",'"',"`"," ","\r",` +`," "],c=["{","}","|","\\","^","`"].concat(o),u=["'"].concat(c),f=["%","/","?",";","#"].concat(u),d=["/","?","#"],m=255,g=/^[+a-z0-9A-Z_-]{0,63}$/,v=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,x={javascript:!0,"javascript:":!0},b={javascript:!0,"javascript:":!0},C={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},T=uR();function S(w,F,E){if(w&&typeof w=="object"&&w instanceof t)return w;var O=new t;return O.parse(w,F,E),O}t.prototype.parse=function(w,F,E){if(typeof w!="string")throw new TypeError("Parameter 'url' must be a string, not "+typeof w);var O=w.indexOf("?"),I=O!==-1&&O127?ct+="x":ct+=st[ft];if(!ct.match(g)){var mt=j.slice(0,N),dt=j.slice(N+1),St=st.match(v);St&&(mt.push(St[1]),dt.unshift(St[2])),dt.length&&(G="/"+dt.join(".")+G),this.hostname=mt.join(".");break}}}this.hostname.length>m?this.hostname="":this.hostname=this.hostname.toLowerCase(),z||(this.hostname=n.toASCII(this.hostname));var bt=this.port?":"+this.port:"",Lt=this.hostname||"";this.host=Lt+bt,this.href+=this.host,z&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),G[0]!=="/"&&(G="/"+G))}if(!x[K])for(var N=0,W=u.length;N0?E.host.split("@"):!1;ct&&(E.auth=ct.shift(),E.hostname=ct.shift(),E.host=E.hostname)}return E.search=w.search,E.query=w.query,(E.pathname!==null||E.search!==null)&&(E.path=(E.pathname?E.pathname:"")+(E.search?E.search:"")),E.href=E.format(),E}if(!j.length)return E.pathname=null,E.search?E.path="/"+E.search:E.path=null,E.href=E.format(),E;for(var ft=j.slice(-1)[0],pt=(E.host||w.host||j.length>1)&&(ft==="."||ft==="..")||ft==="",mt=0,dt=j.length;dt>=0;dt--)ft=j[dt],ft==="."?j.splice(dt,1):ft===".."?(j.splice(dt,1),mt++):mt&&(j.splice(dt,1),mt--);if(!M&&!z)for(;mt--;mt)j.unshift("..");M&&j[0]!==""&&(!j[0]||j[0].charAt(0)!=="/")&&j.unshift(""),pt&&j.join("/").substr(-1)!=="/"&&j.push("");var St=j[0]===""||j[0]&&j[0].charAt(0)==="/";if(st){E.hostname=St?"":j.length?j.shift():"",E.host=E.hostname;var ct=E.host&&E.host.indexOf("@")>0?E.host.split("@"):!1;ct&&(E.auth=ct.shift(),E.hostname=ct.shift(),E.host=E.hostname)}return M=M||E.host&&j.length,M&&!St&&j.unshift(""),j.length>0?E.pathname=j.join("/"):(E.pathname=null,E.path=null),(E.pathname!==null||E.search!==null)&&(E.path=(E.pathname?E.pathname:"")+(E.search?E.search:"")),E.auth=w.auth||E.auth,E.slashes=E.slashes||w.slashes,E.href=E.format(),E},t.prototype.parseHost=function(){var w=this.host,F=i.exec(w);F&&(F=F[0],F!==":"&&(this.port=F.substr(1)),w=w.substr(0,w.length-F.length)),w&&(this.hostname=w)},hr.parse=S,hr.resolve=R,hr.resolveObject=P,hr.format=L,hr.Url=t,hr}hR();const j6={};function he(n,t,e=3){if(j6[t])return;let i=new Error().stack;typeof i>"u"?console.warn("PixiJS Deprecation Warning: ",`${t} +Deprecated since v${n}`):(i=i.split(` +`).splice(e).join(` +`),console.groupCollapsed?(console.groupCollapsed("%cPixiJS Deprecation Warning: %c%s","color:#614108;background:#fffbe6","font-weight:normal;color:#614108;background:#fffbe6",`${t} +Deprecated since v${n}`),console.warn(i),console.groupEnd()):(console.warn("PixiJS Deprecation Warning: ",`${t} +Deprecated since v${n}`),console.warn(i))),j6[t]=!0}let g0;function fR(){return typeof g0>"u"&&(g0=(function(){const n={stencil:!0,failIfMajorPerformanceCaveat:Kt.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT};try{if(!Kt.ADAPTER.getWebGLRenderingContext())return!1;const t=Kt.ADAPTER.createCanvas();let e=t.getContext("webgl",n)||t.getContext("experimental-webgl",n);const i=!!e?.getContextAttributes()?.stencil;if(e){const a=e.getExtension("WEBGL_lose_context");a&&a.loseContext()}return e=null,i}catch{return!1}})()),g0}var dR={grad:.9,turn:360,rad:360/(2*Math.PI)},Ki=function(n){return typeof n=="string"?n.length>0:typeof n=="number"},We=function(n,t,e){return t===void 0&&(t=0),e===void 0&&(e=Math.pow(10,t)),Math.round(e*n)/e+0},$n=function(n,t,e){return t===void 0&&(t=0),e===void 0&&(e=1),n>e?e:n>t?n:t},iv=function(n){return(n=isFinite(n)?n%360:0)>0?n:n+360},H6=function(n){return{r:$n(n.r,0,255),g:$n(n.g,0,255),b:$n(n.b,0,255),a:$n(n.a)}},y0=function(n){return{r:We(n.r),g:We(n.g),b:We(n.b),a:We(n.a,3)}},mR=/^#([0-9a-f]{3,8})$/i,bu=function(n){var t=n.toString(16);return t.length<2?"0"+t:t},sv=function(n){var t=n.r,e=n.g,i=n.b,a=n.a,o=Math.max(t,e,i),c=o-Math.min(t,e,i),u=c?o===t?(e-i)/c:o===e?2+(i-t)/c:4+(t-e)/c:0;return{h:60*(u<0?u+6:u),s:o?c/o*100:0,v:o/255*100,a}},rv=function(n){var t=n.h,e=n.s,i=n.v,a=n.a;t=t/360*6,e/=100,i/=100;var o=Math.floor(t),c=i*(1-e),u=i*(1-(t-o)*e),f=i*(1-(1-t+o)*e),d=o%6;return{r:255*[i,u,c,c,f,i][d],g:255*[f,i,i,u,c,c][d],b:255*[c,c,f,i,i,u][d],a}},q6=function(n){return{h:iv(n.h),s:$n(n.s,0,100),l:$n(n.l,0,100),a:$n(n.a)}},X6=function(n){return{h:We(n.h),s:We(n.s),l:We(n.l),a:We(n.a,3)}},Y6=function(n){return rv((e=(t=n).s,{h:t.h,s:(e*=((i=t.l)<50?i:100-i)/100)>0?2*e/(i+e)*100:0,v:i+e,a:t.a}));var t,e,i},El=function(n){return{h:(t=sv(n)).h,s:(a=(200-(e=t.s))*(i=t.v)/100)>0&&a<200?e*i/100/(a<=100?a:200-a)*100:0,l:a/2,a:t.a};var t,e,i,a},pR=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,gR=/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,yR=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,vR=/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i,gm={string:[[function(n){var t=mR.exec(n);return t?(n=t[1]).length<=4?{r:parseInt(n[0]+n[0],16),g:parseInt(n[1]+n[1],16),b:parseInt(n[2]+n[2],16),a:n.length===4?We(parseInt(n[3]+n[3],16)/255,2):1}:n.length===6||n.length===8?{r:parseInt(n.substr(0,2),16),g:parseInt(n.substr(2,2),16),b:parseInt(n.substr(4,2),16),a:n.length===8?We(parseInt(n.substr(6,2),16)/255,2):1}:null:null},"hex"],[function(n){var t=yR.exec(n)||vR.exec(n);return t?t[2]!==t[4]||t[4]!==t[6]?null:H6({r:Number(t[1])/(t[2]?100/255:1),g:Number(t[3])/(t[4]?100/255:1),b:Number(t[5])/(t[6]?100/255:1),a:t[7]===void 0?1:Number(t[7])/(t[8]?100:1)}):null},"rgb"],[function(n){var t=pR.exec(n)||gR.exec(n);if(!t)return null;var e,i,a=q6({h:(e=t[1],i=t[2],i===void 0&&(i="deg"),Number(e)*(dR[i]||1)),s:Number(t[3]),l:Number(t[4]),a:t[5]===void 0?1:Number(t[5])/(t[6]?100:1)});return Y6(a)},"hsl"]],object:[[function(n){var t=n.r,e=n.g,i=n.b,a=n.a,o=a===void 0?1:a;return Ki(t)&&Ki(e)&&Ki(i)?H6({r:Number(t),g:Number(e),b:Number(i),a:Number(o)}):null},"rgb"],[function(n){var t=n.h,e=n.s,i=n.l,a=n.a,o=a===void 0?1:a;if(!Ki(t)||!Ki(e)||!Ki(i))return null;var c=q6({h:Number(t),s:Number(e),l:Number(i),a:Number(o)});return Y6(c)},"hsl"],[function(n){var t=n.h,e=n.s,i=n.v,a=n.a,o=a===void 0?1:a;if(!Ki(t)||!Ki(e)||!Ki(i))return null;var c=(function(u){return{h:iv(u.h),s:$n(u.s,0,100),v:$n(u.v,0,100),a:$n(u.a)}})({h:Number(t),s:Number(e),v:Number(i),a:Number(o)});return rv(c)},"hsv"]]},$6=function(n,t){for(var e=0;e=.5},n.prototype.toHex=function(){return t=y0(this.rgba),e=t.r,i=t.g,a=t.b,c=(o=t.a)<1?bu(We(255*o)):"","#"+bu(e)+bu(i)+bu(a)+c;var t,e,i,a,o,c},n.prototype.toRgb=function(){return y0(this.rgba)},n.prototype.toRgbString=function(){return t=y0(this.rgba),e=t.r,i=t.g,a=t.b,(o=t.a)<1?"rgba("+e+", "+i+", "+a+", "+o+")":"rgb("+e+", "+i+", "+a+")";var t,e,i,a,o},n.prototype.toHsl=function(){return X6(El(this.rgba))},n.prototype.toHslString=function(){return t=X6(El(this.rgba)),e=t.h,i=t.s,a=t.l,(o=t.a)<1?"hsla("+e+", "+i+"%, "+a+"%, "+o+")":"hsl("+e+", "+i+"%, "+a+"%)";var t,e,i,a,o},n.prototype.toHsv=function(){return t=sv(this.rgba),{h:We(t.h),s:We(t.s),v:We(t.v),a:We(t.a,3)};var t},n.prototype.invert=function(){return vi({r:255-(t=this.rgba).r,g:255-t.g,b:255-t.b,a:t.a});var t},n.prototype.saturate=function(t){return t===void 0&&(t=.1),vi(v0(this.rgba,t))},n.prototype.desaturate=function(t){return t===void 0&&(t=.1),vi(v0(this.rgba,-t))},n.prototype.grayscale=function(){return vi(v0(this.rgba,-1))},n.prototype.lighten=function(t){return t===void 0&&(t=.1),vi(W6(this.rgba,t))},n.prototype.darken=function(t){return t===void 0&&(t=.1),vi(W6(this.rgba,-t))},n.prototype.rotate=function(t){return t===void 0&&(t=15),this.hue(this.hue()+t)},n.prototype.alpha=function(t){return typeof t=="number"?vi({r:(e=this.rgba).r,g:e.g,b:e.b,a:t}):We(this.rgba.a,3);var e},n.prototype.hue=function(t){var e=El(this.rgba);return typeof t=="number"?vi({h:t,s:e.s,l:e.l,a:e.a}):We(e.h)},n.prototype.isEqual=function(t){return this.toHex()===vi(t).toHex()},n})(),vi=function(n){return n instanceof ym?n:new ym(n)},Z6=[],xR=function(n){n.forEach(function(t){Z6.indexOf(t)<0&&(t(ym,gm),Z6.push(t))})};function TR(n,t){var e={white:"#ffffff",bisque:"#ffe4c4",blue:"#0000ff",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",antiquewhite:"#faebd7",aqua:"#00ffff",azure:"#f0ffff",whitesmoke:"#f5f5f5",papayawhip:"#ffefd5",plum:"#dda0dd",blanchedalmond:"#ffebcd",black:"#000000",gold:"#ffd700",goldenrod:"#daa520",gainsboro:"#dcdcdc",cornsilk:"#fff8dc",cornflowerblue:"#6495ed",burlywood:"#deb887",aquamarine:"#7fffd4",beige:"#f5f5dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkkhaki:"#bdb76b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",peachpuff:"#ffdab9",darkmagenta:"#8b008b",darkred:"#8b0000",darkorchid:"#9932cc",darkorange:"#ff8c00",darkslateblue:"#483d8b",gray:"#808080",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",deeppink:"#ff1493",deepskyblue:"#00bfff",wheat:"#f5deb3",firebrick:"#b22222",floralwhite:"#fffaf0",ghostwhite:"#f8f8ff",darkviolet:"#9400d3",magenta:"#ff00ff",green:"#008000",dodgerblue:"#1e90ff",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",blueviolet:"#8a2be2",forestgreen:"#228b22",lawngreen:"#7cfc00",indianred:"#cd5c5c",indigo:"#4b0082",fuchsia:"#ff00ff",brown:"#a52a2a",maroon:"#800000",mediumblue:"#0000cd",lightcoral:"#f08080",darkturquoise:"#00ced1",lightcyan:"#e0ffff",ivory:"#fffff0",lightyellow:"#ffffe0",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",linen:"#faf0e6",mediumaquamarine:"#66cdaa",lemonchiffon:"#fffacd",lime:"#00ff00",khaki:"#f0e68c",mediumseagreen:"#3cb371",limegreen:"#32cd32",mediumspringgreen:"#00fa9a",lightskyblue:"#87cefa",lightblue:"#add8e6",midnightblue:"#191970",lightpink:"#ffb6c1",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",mintcream:"#f5fffa",lightslategray:"#778899",lightslategrey:"#778899",navajowhite:"#ffdead",navy:"#000080",mediumvioletred:"#c71585",powderblue:"#b0e0e6",palegoldenrod:"#eee8aa",oldlace:"#fdf5e6",paleturquoise:"#afeeee",mediumturquoise:"#48d1cc",mediumorchid:"#ba55d3",rebeccapurple:"#663399",lightsteelblue:"#b0c4de",mediumslateblue:"#7b68ee",thistle:"#d8bfd8",tan:"#d2b48c",orchid:"#da70d6",mediumpurple:"#9370db",purple:"#800080",pink:"#ffc0cb",skyblue:"#87ceeb",springgreen:"#00ff7f",palegreen:"#98fb98",red:"#ff0000",yellow:"#ffff00",slateblue:"#6a5acd",lavenderblush:"#fff0f5",peru:"#cd853f",palevioletred:"#db7093",violet:"#ee82ee",teal:"#008080",slategray:"#708090",slategrey:"#708090",aliceblue:"#f0f8ff",darkseagreen:"#8fbc8f",darkolivegreen:"#556b2f",greenyellow:"#adff2f",seagreen:"#2e8b57",seashell:"#fff5ee",tomato:"#ff6347",silver:"#c0c0c0",sienna:"#a0522d",lavender:"#e6e6fa",lightgreen:"#90ee90",orange:"#ffa500",orangered:"#ff4500",steelblue:"#4682b4",royalblue:"#4169e1",turquoise:"#40e0d0",yellowgreen:"#9acd32",salmon:"#fa8072",saddlebrown:"#8b4513",sandybrown:"#f4a460",rosybrown:"#bc8f8f",darksalmon:"#e9967a",lightgoldenrodyellow:"#fafad2",snow:"#fffafa",lightgrey:"#d3d3d3",lightgray:"#d3d3d3",dimgray:"#696969",dimgrey:"#696969",olivedrab:"#6b8e23",olive:"#808000"},i={};for(var a in e)i[e[a]]=a;var o={};n.prototype.toName=function(c){if(!(this.rgba.a||this.rgba.r||this.rgba.g||this.rgba.b))return"transparent";var u,f,d=i[this.toHex()];if(d)return d;if(c?.closest){var m=this.toRgb(),g=1/0,v="black";if(!o.length)for(var x in e)o[x]=new n(e[x]).toRgb();for(var b in e){var C=(u=m,f=o[b],Math.pow(u.r-f.r,2)+Math.pow(u.g-f.g,2)+Math.pow(u.b-f.b,2));Ca===e[o]);if(t!==null&&e!==null){const a=Object.keys(t),o=Object.keys(e);return a.length!==o.length?!1:a.every(c=>t[c]===e[c])}return t===e}toRgba(){const[t,e,i,a]=this._components;return{r:t,g:e,b:i,a}}toRgb(){const[t,e,i]=this._components;return{r:t,g:e,b:i}}toRgbaString(){const[t,e,i]=this.toUint8RgbArray();return`rgba(${t},${e},${i},${this.alpha})`}toUint8RgbArray(t){const[e,i,a]=this._components;return t=t??[],t[0]=Math.round(e*255),t[1]=Math.round(i*255),t[2]=Math.round(a*255),t}toRgbArray(t){t=t??[];const[e,i,a]=this._components;return t[0]=e,t[1]=i,t[2]=a,t}toNumber(){return this._int}toLittleEndianNumber(){const t=this._int;return(t>>16)+(t&65280)+((t&255)<<16)}multiply(t){const[e,i,a,o]=Hu.temp.setValue(t)._components;return this._components[0]*=e,this._components[1]*=i,this._components[2]*=a,this._components[3]*=o,this.refreshInt(),this._value=null,this}premultiply(t,e=!0){return e&&(this._components[0]*=t,this._components[1]*=t,this._components[2]*=t),this._components[3]=t,this.refreshInt(),this._value=null,this}toPremultiplied(t,e=!0){if(t===1)return(255<<24)+this._int;if(t===0)return e?0:this._int;let i=this._int>>16&255,a=this._int>>8&255,o=this._int&255;return e&&(i=i*t+.5|0,a=a*t+.5|0,o=o*t+.5|0),(t*255<<24)+(i<<16)+(a<<8)+o}toHex(){const t=this._int.toString(16);return`#${"000000".substring(0,6-t.length)+t}`}toHexa(){const t=Math.round(this._components[3]*255).toString(16);return this.toHex()+"00".substring(0,2-t.length)+t}setAlpha(t){return this._components[3]=this._clamp(t),this}round(t){const[e,i,a]=this._components;return this._components[0]=Math.round(e*t)/t,this._components[1]=Math.round(i*t)/t,this._components[2]=Math.round(a*t)/t,this.refreshInt(),this._value=null,this}toArray(t){t=t??[];const[e,i,a,o]=this._components;return t[0]=e,t[1]=i,t[2]=a,t[3]=o,t}normalize(t){let e,i,a,o;if((typeof t=="number"||t instanceof Number)&&t>=0&&t<=16777215){const c=t;e=(c>>16&255)/255,i=(c>>8&255)/255,a=(c&255)/255,o=1}else if((Array.isArray(t)||t instanceof Float32Array)&&t.length>=3&&t.length<=4)t=this._clamp(t),[e,i,a,o=1]=t;else if((t instanceof Uint8Array||t instanceof Uint8ClampedArray)&&t.length>=3&&t.length<=4)t=this._clamp(t,0,255),[e,i,a,o=255]=t,e/=255,i/=255,a/=255,o/=255;else if(typeof t=="string"||typeof t=="object"){if(typeof t=="string"){const u=Hu.HEX_PATTERN.exec(t);u&&(t=`#${u[2]}`)}const c=vi(t);c.isValid()&&({r:e,g:i,b:a,a:o}=c.rgba,e/=255,i/=255,a/=255)}if(e!==void 0)this._components[0]=e,this._components[1]=i,this._components[2]=a,this._components[3]=o,this.refreshInt();else throw new Error(`Unable to convert color ${t}`)}refreshInt(){this._clamp(this._components);const[t,e,i]=this._components;this._int=(t*255<<16)+(e*255<<8)+(i*255|0)}_clamp(t,e=0,i=1){return typeof t=="number"?Math.min(Math.max(t,e),i):(t.forEach((a,o)=>{t[o]=Math.min(Math.max(a,e),i)}),t)}};Oa.shared=new Oa,Oa.temp=new Oa,Oa.HEX_PATTERN=/^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;let Or=Oa;function CR(){const n=[],t=[];for(let i=0;i<32;i++)n[i]=i,t[i]=i;n[Nt.NORMAL_NPM]=Nt.NORMAL,n[Nt.ADD_NPM]=Nt.ADD,n[Nt.SCREEN_NPM]=Nt.SCREEN,t[Nt.NORMAL]=Nt.NORMAL_NPM,t[Nt.ADD]=Nt.ADD_NPM,t[Nt.SCREEN]=Nt.SCREEN_NPM;const e=[];return e.push(t),e.push(n),e}const SR=CR();function av(n){if(n.BYTES_PER_ELEMENT===4)return n instanceof Float32Array?"Float32Array":n instanceof Uint32Array?"Uint32Array":"Int32Array";if(n.BYTES_PER_ELEMENT===2){if(n instanceof Uint16Array)return"Uint16Array"}else if(n.BYTES_PER_ELEMENT===1&&n instanceof Uint8Array)return"Uint8Array";return null}function ch(n){return n+=n===0?1:0,--n,n|=n>>>1,n|=n>>>2,n|=n>>>4,n|=n>>>8,n|=n>>>16,n+1}function K6(n){return!(n&n-1)&&!!n}function Q6(n){let t=(n>65535?1:0)<<4;n>>>=t;let e=(n>255?1:0)<<3;return n>>>=e,t|=e,e=(n>15?1:0)<<2,n>>>=e,t|=e,e=(n>3?1:0)<<1,n>>>=e,t|=e,t|n>>1}function qu(n,t,e){const i=n.length;let a;if(t>=i||e===0)return;e=t+e>i?i-t:e;const o=i-e;for(a=t;a(n.Renderer="renderer",n.Application="application",n.RendererSystem="renderer-webgl-system",n.RendererPlugin="renderer-webgl-plugin",n.CanvasRendererSystem="renderer-canvas-system",n.CanvasRendererPlugin="renderer-canvas-plugin",n.Asset="asset",n.LoadParser="load-parser",n.ResolveParser="resolve-parser",n.CacheParser="cache-parser",n.DetectionParser="detection-parser",n))(Ht||{});const vm=n=>{if(typeof n=="function"||typeof n=="object"&&n.extension){if(!n.extension)throw new Error("Extension class must have an extension object");n={...typeof n.extension!="object"?{type:n.extension}:n.extension,ref:n}}if(typeof n=="object")n={...n};else throw new Error("Invalid extension type");return typeof n.type=="string"&&(n.type=[n.type]),n},e8=(n,t)=>vm(n).priority??t,ae={_addHandlers:{},_removeHandlers:{},_queue:{},remove(...n){return n.map(vm).forEach(t=>{t.type.forEach(e=>this._removeHandlers[e]?.(t))}),this},add(...n){return n.map(vm).forEach(t=>{t.type.forEach(e=>{const i=this._addHandlers,a=this._queue;i[e]?i[e]?.(t):(a[e]=a[e]||[],a[e]?.push(t))})}),this},handle(n,t,e){const i=this._addHandlers,a=this._removeHandlers;if(i[n]||a[n])throw new Error(`Extension type ${n} already has a handler`);i[n]=t,a[n]=e;const o=this._queue;return o[n]&&(o[n]?.forEach(c=>t(c)),delete o[n]),this},handleByMap(n,t){return this.handle(n,e=>{e.name&&(t[e.name]=e.ref)},e=>{e.name&&delete t[e.name]})},handleByList(n,t,e=-1){return this.handle(n,i=>{t.includes(i.ref)||(t.push(i.ref),t.sort((a,o)=>e8(o,e)-e8(a,e)))},i=>{const a=t.indexOf(i.ref);a!==-1&&t.splice(a,1)})}};class ER{constructor(t){typeof t=="number"?this.rawBinaryData=new ArrayBuffer(t):t instanceof Uint8Array?this.rawBinaryData=t.buffer:this.rawBinaryData=t,this.uint32View=new Uint32Array(this.rawBinaryData),this.float32View=new Float32Array(this.rawBinaryData)}get int8View(){return this._int8View||(this._int8View=new Int8Array(this.rawBinaryData)),this._int8View}get uint8View(){return this._uint8View||(this._uint8View=new Uint8Array(this.rawBinaryData)),this._uint8View}get int16View(){return this._int16View||(this._int16View=new Int16Array(this.rawBinaryData)),this._int16View}get uint16View(){return this._uint16View||(this._uint16View=new Uint16Array(this.rawBinaryData)),this._uint16View}get int32View(){return this._int32View||(this._int32View=new Int32Array(this.rawBinaryData)),this._int32View}view(t){return this[`${t}View`]}destroy(){this.rawBinaryData=null,this._int8View=null,this._uint8View=null,this._int16View=null,this._uint16View=null,this._int32View=null,this.uint32View=null,this.float32View=null}static sizeOf(t){switch(t){case"int8":case"uint8":return 1;case"int16":case"uint16":return 2;case"int32":case"uint32":case"float32":return 4;default:throw new Error(`${t} isn't a valid view type`)}}}const wR=["precision mediump float;","void main(void){","float test = 0.1;","%forloop%","gl_FragColor = vec4(0.0);","}"].join(` +`);function MR(n){let t="";for(let e=0;e0&&(t+=` +else `),e=0;--i){const a=bm[i];if(a.test&&a.test(n,e))return new a(n,t)}throw new Error("Unrecognized source type to auto-detect Resource")}class ui{constructor(t){this.items=[],this._name=t,this._aliasCount=0}emit(t,e,i,a,o,c,u,f){if(arguments.length>8)throw new Error("max arguments reached");const{name:d,items:m}=this;this._aliasCount++;for(let g=0,v=m.length;g0&&this.items.length>1&&(this._aliasCount=0,this.items=this.items.slice(0))}add(t){return t[this._name]&&(this.ensureNonAliasedItems(),this.remove(t),this.items.push(t)),this}remove(t){const e=this.items.indexOf(t);return e!==-1&&(this.ensureNonAliasedItems(),this.items.splice(e,1)),this}contains(t){return this.items.includes(t)}removeAll(){return this.ensureNonAliasedItems(),this.items.length=0,this}destroy(){this.removeAll(),this.items.length=0,this._name=""}get empty(){return this.items.length===0}get name(){return this._name}}Object.defineProperties(ui.prototype,{dispatch:{value:ui.prototype.emit},run:{value:ui.prototype.emit}});class kl{constructor(t=0,e=0){this._width=t,this._height=e,this.destroyed=!1,this.internal=!1,this.onResize=new ui("setRealSize"),this.onUpdate=new ui("update"),this.onError=new ui("onError")}bind(t){this.onResize.add(t),this.onUpdate.add(t),this.onError.add(t),(this._width||this._height)&&this.onResize.emit(this._width,this._height)}unbind(t){this.onResize.remove(t),this.onUpdate.remove(t),this.onError.remove(t)}resize(t,e){(t!==this._width||e!==this._height)&&(this._width=t,this._height=e,this.onResize.emit(t,e))}get valid(){return!!this._width&&!!this._height}update(){this.destroyed||this.onUpdate.emit()}load(){return Promise.resolve(this)}get width(){return this._width}get height(){return this._height}style(t,e,i){return!1}dispose(){}destroy(){this.destroyed||(this.destroyed=!0,this.dispose(),this.onError.removeAll(),this.onError=null,this.onResize.removeAll(),this.onResize=null,this.onUpdate.removeAll(),this.onUpdate=null)}static test(t,e){return!1}}class lv extends kl{constructor(t,e){const{width:i,height:a}=e||{};if(!i||!a)throw new Error("BufferResource width or height invalid");super(i,a),this.data=t,this.unpackAlignment=e.unpackAlignment??4}upload(t,e,i){const a=t.gl;a.pixelStorei(a.UNPACK_ALIGNMENT,this.unpackAlignment),a.pixelStorei(a.UNPACK_PREMULTIPLY_ALPHA_WEBGL,e.alphaMode===Pr.UNPACK);const o=e.realWidth,c=e.realHeight;return i.width===o&&i.height===c?a.texSubImage2D(e.target,0,0,0,o,c,e.format,i.type,this.data):(i.width=o,i.height=c,a.texImage2D(e.target,0,i.internalFormat,o,c,0,e.format,i.type,this.data)),!0}dispose(){this.data=null}static test(t){return t===null||t instanceof Int8Array||t instanceof Uint8Array||t instanceof Uint8ClampedArray||t instanceof Int16Array||t instanceof Uint16Array||t instanceof Int32Array||t instanceof Uint32Array||t instanceof Float32Array}}const LR={scaleMode:es.NEAREST,alphaMode:Pr.NPM},xm=class Ia extends Eh{constructor(t=null,e=null){super(),e=Object.assign({},Ia.defaultOptions,e);const{alphaMode:i,mipmap:a,anisotropicLevel:o,scaleMode:c,width:u,height:f,wrapMode:d,format:m,type:g,target:v,resolution:x,resourceOptions:b}=e;t&&!(t instanceof kl)&&(t=ov(t,b),t.internal=!0),this.resolution=x||Kt.RESOLUTION,this.width=Math.round((u||0)*this.resolution)/this.resolution,this.height=Math.round((f||0)*this.resolution)/this.resolution,this._mipmap=a,this.anisotropicLevel=o,this._wrapMode=d,this._scaleMode=c,this.format=m,this.type=g,this.target=v,this.alphaMode=i,this.uid=Ul(),this.touched=0,this.isPowerOfTwo=!1,this._refreshPOT(),this._glTextures={},this.dirtyId=0,this.dirtyStyleId=0,this.cacheId=null,this.valid=u>0&&f>0,this.textureCacheIds=[],this.destroyed=!1,this.resource=null,this._batchEnabled=0,this._batchLocation=0,this.parentTextureArray=null,this.setResource(t)}get realWidth(){return Math.round(this.width*this.resolution)}get realHeight(){return Math.round(this.height*this.resolution)}get mipmap(){return this._mipmap}set mipmap(t){this._mipmap!==t&&(this._mipmap=t,this.dirtyStyleId++)}get scaleMode(){return this._scaleMode}set scaleMode(t){this._scaleMode!==t&&(this._scaleMode=t,this.dirtyStyleId++)}get wrapMode(){return this._wrapMode}set wrapMode(t){this._wrapMode!==t&&(this._wrapMode=t,this.dirtyStyleId++)}setStyle(t,e){let i;return t!==void 0&&t!==this.scaleMode&&(this.scaleMode=t,i=!0),e!==void 0&&e!==this.mipmap&&(this.mipmap=e,i=!0),i&&this.dirtyStyleId++,this}setSize(t,e,i){return i=i||this.resolution,this.setRealSize(t*i,e*i,i)}setRealSize(t,e,i){return this.resolution=i||this.resolution,this.width=Math.round(t)/this.resolution,this.height=Math.round(e)/this.resolution,this._refreshPOT(),this.update(),this}_refreshPOT(){this.isPowerOfTwo=K6(this.realWidth)&&K6(this.realHeight)}setResolution(t){const e=this.resolution;return e===t?this:(this.resolution=t,this.valid&&(this.width=Math.round(this.width*e)/t,this.height=Math.round(this.height*e)/t,this.emit("update",this)),this._refreshPOT(),this)}setResource(t){if(this.resource===t)return this;if(this.resource)throw new Error("Resource can be set only once");return t.bind(this),this.resource=t,this}update(){this.valid?(this.dirtyId++,this.dirtyStyleId++,this.emit("update",this)):this.width>0&&this.height>0&&(this.valid=!0,this.emit("loaded",this),this.emit("update",this))}onError(t){this.emit("error",this,t)}destroy(){this.resource&&(this.resource.unbind(this),this.resource.internal&&this.resource.destroy(),this.resource=null),this.cacheId&&(delete Os[this.cacheId],delete bi[this.cacheId],this.cacheId=null),this.valid=!1,this.dispose(),Ia.removeFromCache(this),this.textureCacheIds=null,this.destroyed=!0,this.emit("destroyed",this),this.removeAllListeners()}dispose(){this.emit("dispose",this)}castToBaseTexture(){return this}static from(t,e,i=Kt.STRICT_TEXTURE_CACHE){const a=typeof t=="string";let o=null;if(a)o=t;else{if(!t._pixiId){const u=e?.pixiIdPrefix||"pixiid";t._pixiId=`${u}_${Ul()}`}o=t._pixiId}let c=Os[o];if(a&&i&&!c)throw new Error(`The cacheId "${o}" does not exist in BaseTextureCache.`);return c||(c=new Ia(t,e),c.cacheId=o,Ia.addToCache(c,o)),c}static fromBuffer(t,e,i,a){t=t||new Float32Array(e*i*4);const o=new lv(t,{width:e,height:i,...a?.resourceOptions});let c,u;return t instanceof Float32Array?(c=gt.RGBA,u=jt.FLOAT):t instanceof Int32Array?(c=gt.RGBA_INTEGER,u=jt.INT):t instanceof Uint32Array?(c=gt.RGBA_INTEGER,u=jt.UNSIGNED_INT):t instanceof Int16Array?(c=gt.RGBA_INTEGER,u=jt.SHORT):t instanceof Uint16Array?(c=gt.RGBA_INTEGER,u=jt.UNSIGNED_SHORT):t instanceof Int8Array?(c=gt.RGBA,u=jt.BYTE):(c=gt.RGBA,u=jt.UNSIGNED_BYTE),o.internal=!0,new Ia(o,Object.assign({},LR,{type:u,format:c},a))}static addToCache(t,e){e&&(t.textureCacheIds.includes(e)||t.textureCacheIds.push(e),Os[e]&&Os[e]!==t&&console.warn(`BaseTexture added to the cache with an id [${e}] that already had an entry`),Os[e]=t)}static removeFromCache(t){if(typeof t=="string"){const e=Os[t];if(e){const i=e.textureCacheIds.indexOf(t);return i>-1&&e.textureCacheIds.splice(i,1),delete Os[t],e}}else if(t?.textureCacheIds){for(let e=0;e1){for(let g=0;g(n[n.POLY=0]="POLY",n[n.RECT=1]="RECT",n[n.CIRC=2]="CIRC",n[n.ELIP=3]="ELIP",n[n.RREC=4]="RREC",n))(cv||{});class an{constructor(t=0,e=0){this.x=0,this.y=0,this.x=t,this.y=e}clone(){return new an(this.x,this.y)}copyFrom(t){return this.set(t.x,t.y),this}copyTo(t){return t.set(this.x,this.y),t}equals(t){return t.x===this.x&&t.y===this.y}set(t=0,e=t){return this.x=t,this.y=e,this}}an.prototype.toString=function(){return`[@pixi/math:Point x=${this.x} y=${this.y}]`};const Tu=[new an,new an,new an,new an];class be{constructor(t=0,e=0,i=0,a=0){this.x=Number(t),this.y=Number(e),this.width=Number(i),this.height=Number(a),this.type=cv.RECT}get left(){return this.x}get right(){return this.x+this.width}get top(){return this.y}get bottom(){return this.y+this.height}static get EMPTY(){return new be(0,0,0,0)}clone(){return new be(this.x,this.y,this.width,this.height)}copyFrom(t){return this.x=t.x,this.y=t.y,this.width=t.width,this.height=t.height,this}copyTo(t){return t.x=this.x,t.y=this.y,t.width=this.width,t.height=this.height,t}contains(t,e){return this.width<=0||this.height<=0?!1:t>=this.x&&t=this.y&&et.right?t.right:this.right)<=O)return!1;const I=this.yt.bottom?t.bottom:this.bottom)>I}const i=this.left,a=this.right,o=this.top,c=this.bottom;if(a<=i||c<=o)return!1;const u=Tu[0].set(t.left,t.top),f=Tu[1].set(t.left,t.bottom),d=Tu[2].set(t.right,t.top),m=Tu[3].set(t.right,t.bottom);if(d.x<=u.x||f.y<=u.y)return!1;const g=Math.sign(e.a*e.d-e.b*e.c);if(g===0||(e.apply(u,u),e.apply(f,f),e.apply(d,d),e.apply(m,m),Math.max(u.x,f.x,d.x,m.x)<=i||Math.min(u.x,f.x,d.x,m.x)>=a||Math.max(u.y,f.y,d.y,m.y)<=o||Math.min(u.y,f.y,d.y,m.y)>=c))return!1;const v=g*(f.y-u.y),x=g*(u.x-f.x),b=v*i+x*o,C=v*a+x*o,T=v*i+x*c,S=v*a+x*c;if(Math.max(b,C,T,S)<=v*u.x+x*u.y||Math.min(b,C,T,S)>=v*m.x+x*m.y)return!1;const L=g*(u.y-d.y),R=g*(d.x-u.x),P=L*i+R*o,w=L*a+R*o,F=L*i+R*c,E=L*a+R*c;return!(Math.max(P,w,F,E)<=L*u.x+R*u.y||Math.min(P,w,F,E)>=L*m.x+R*m.y)}pad(t=0,e=t){return this.x-=t,this.y-=e,this.width+=t*2,this.height+=e*2,this}fit(t){const e=Math.max(this.x,t.x),i=Math.min(this.x+this.width,t.x+t.width),a=Math.max(this.y,t.y),o=Math.min(this.y+this.height,t.y+t.height);return this.x=e,this.width=Math.max(i-e,0),this.y=a,this.height=Math.max(o-a,0),this}ceil(t=1,e=.001){const i=Math.ceil((this.x+this.width-e)*t)/t,a=Math.ceil((this.y+this.height-e)*t)/t;return this.x=Math.floor((this.x+e)*t)/t,this.y=Math.floor((this.y+e)*t)/t,this.width=i-this.x,this.height=a-this.y,this}enlarge(t){const e=Math.min(this.x,t.x),i=Math.max(this.x+this.width,t.x+t.width),a=Math.min(this.y,t.y),o=Math.max(this.y+this.height,t.y+t.height);return this.x=e,this.width=i-e,this.y=a,this.height=o-a,this}}be.prototype.toString=function(){return`[@pixi/math:Rectangle x=${this.x} y=${this.y} width=${this.width} height=${this.height}]`};class qe{constructor(t=1,e=0,i=0,a=1,o=0,c=0){this.array=null,this.a=t,this.b=e,this.c=i,this.d=a,this.tx=o,this.ty=c}fromArray(t){this.a=t[0],this.b=t[1],this.c=t[3],this.d=t[4],this.tx=t[2],this.ty=t[5]}set(t,e,i,a,o,c){return this.a=t,this.b=e,this.c=i,this.d=a,this.tx=o,this.ty=c,this}toArray(t,e){this.array||(this.array=new Float32Array(9));const i=e||this.array;return t?(i[0]=this.a,i[1]=this.b,i[2]=0,i[3]=this.c,i[4]=this.d,i[5]=0,i[6]=this.tx,i[7]=this.ty,i[8]=1):(i[0]=this.a,i[1]=this.c,i[2]=this.tx,i[3]=this.b,i[4]=this.d,i[5]=this.ty,i[6]=0,i[7]=0,i[8]=1),i}apply(t,e){e=e||new an;const i=t.x,a=t.y;return e.x=this.a*i+this.c*a+this.tx,e.y=this.b*i+this.d*a+this.ty,e}applyInverse(t,e){e=e||new an;const i=1/(this.a*this.d+this.c*-this.b),a=t.x,o=t.y;return e.x=this.d*i*a+-this.c*i*o+(this.ty*this.c-this.tx*this.d)*i,e.y=this.a*i*o+-this.b*i*a+(-this.ty*this.a+this.tx*this.b)*i,e}translate(t,e){return this.tx+=t,this.ty+=e,this}scale(t,e){return this.a*=t,this.d*=e,this.c*=t,this.b*=e,this.tx*=t,this.ty*=e,this}rotate(t){const e=Math.cos(t),i=Math.sin(t),a=this.a,o=this.c,c=this.tx;return this.a=a*e-this.b*i,this.b=a*i+this.b*e,this.c=o*e-this.d*i,this.d=o*i+this.d*e,this.tx=c*e-this.ty*i,this.ty=c*i+this.ty*e,this}append(t){const e=this.a,i=this.b,a=this.c,o=this.d;return this.a=t.a*e+t.b*a,this.b=t.a*i+t.b*o,this.c=t.c*e+t.d*a,this.d=t.c*i+t.d*o,this.tx=t.tx*e+t.ty*a+this.tx,this.ty=t.tx*i+t.ty*o+this.ty,this}setTransform(t,e,i,a,o,c,u,f,d){return this.a=Math.cos(u+d)*o,this.b=Math.sin(u+d)*o,this.c=-Math.sin(u-f)*c,this.d=Math.cos(u-f)*c,this.tx=t-(i*this.a+a*this.c),this.ty=e-(i*this.b+a*this.d),this}prepend(t){const e=this.tx;if(t.a!==1||t.b!==0||t.c!==0||t.d!==1){const i=this.a,a=this.c;this.a=i*t.a+this.b*t.c,this.b=i*t.b+this.b*t.d,this.c=a*t.a+this.d*t.c,this.d=a*t.b+this.d*t.d}return this.tx=e*t.a+this.ty*t.c+t.tx,this.ty=e*t.b+this.ty*t.d+t.ty,this}decompose(t){const e=this.a,i=this.b,a=this.c,o=this.d,c=t.pivot,u=-Math.atan2(-a,o),f=Math.atan2(i,e),d=Math.abs(u+f);return d<1e-5||Math.abs(UR-d)<1e-5?(t.rotation=f,t.skew.x=t.skew.y=0):(t.rotation=0,t.skew.x=u,t.skew.y=f),t.scale.x=Math.sqrt(e*e+i*i),t.scale.y=Math.sqrt(a*a+o*o),t.position.x=this.tx+(c.x*e+c.y*a),t.position.y=this.ty+(c.x*i+c.y*o),t}invert(){const t=this.a,e=this.b,i=this.c,a=this.d,o=this.tx,c=t*a-e*i;return this.a=a/c,this.b=-e/c,this.c=-i/c,this.d=t/c,this.tx=(i*this.ty-a*o)/c,this.ty=-(t*this.ty-e*o)/c,this}identity(){return this.a=1,this.b=0,this.c=0,this.d=1,this.tx=0,this.ty=0,this}clone(){const t=new qe;return t.a=this.a,t.b=this.b,t.c=this.c,t.d=this.d,t.tx=this.tx,t.ty=this.ty,t}copyTo(t){return t.a=this.a,t.b=this.b,t.c=this.c,t.d=this.d,t.tx=this.tx,t.ty=this.ty,t}copyFrom(t){return this.a=t.a,this.b=t.b,this.c=t.c,this.d=t.d,this.tx=t.tx,this.ty=t.ty,this}static get IDENTITY(){return new qe}static get TEMP_MATRIX(){return new qe}}qe.prototype.toString=function(){return`[@pixi/math:Matrix a=${this.a} b=${this.b} c=${this.c} d=${this.d} tx=${this.tx} ty=${this.ty}]`};const gr=[1,1,0,-1,-1,-1,0,1,1,1,0,-1,-1,-1,0,1],yr=[0,1,1,1,0,-1,-1,-1,0,1,1,1,0,-1,-1,-1],vr=[0,-1,-1,-1,0,1,1,1,0,1,1,1,0,-1,-1,-1],br=[1,1,0,-1,-1,-1,0,1,-1,-1,0,1,1,1,0,-1],Tm=[],uv=[],Cu=Math.sign;function VR(){for(let n=0;n<16;n++){const t=[];Tm.push(t);for(let e=0;e<16;e++){const i=Cu(gr[n]*gr[e]+vr[n]*yr[e]),a=Cu(yr[n]*gr[e]+br[n]*yr[e]),o=Cu(gr[n]*vr[e]+vr[n]*br[e]),c=Cu(yr[n]*vr[e]+br[n]*br[e]);for(let u=0;u<16;u++)if(gr[u]===i&&yr[u]===a&&vr[u]===o&&br[u]===c){t.push(u);break}}}for(let n=0;n<16;n++){const t=new qe;t.set(gr[n],yr[n],vr[n],br[n],0,0),uv.push(t)}}VR();const _e={E:0,SE:1,S:2,SW:3,W:4,NW:5,N:6,NE:7,MIRROR_VERTICAL:8,MAIN_DIAGONAL:10,MIRROR_HORIZONTAL:12,REVERSE_DIAGONAL:14,uX:n=>gr[n],uY:n=>yr[n],vX:n=>vr[n],vY:n=>br[n],inv:n=>n&8?n&15:-n&7,add:(n,t)=>Tm[n][t],sub:(n,t)=>Tm[n][_e.inv(t)],rotate180:n=>n^4,isVertical:n=>(n&3)===2,byDirection:(n,t)=>Math.abs(n)*2<=Math.abs(t)?t>=0?_e.S:_e.N:Math.abs(t)*2<=Math.abs(n)?n>0?_e.E:_e.W:t>0?n>0?_e.SE:_e.SW:n>0?_e.NE:_e.NW,matrixAppendRotationInv:(n,t,e=0,i=0)=>{const a=uv[_e.inv(t)];a.tx=e,a.ty=i,n.append(a)}};class Fs{constructor(t,e,i=0,a=0){this._x=i,this._y=a,this.cb=t,this.scope=e}clone(t=this.cb,e=this.scope){return new Fs(t,e,this._x,this._y)}set(t=0,e=t){return(this._x!==t||this._y!==e)&&(this._x=t,this._y=e,this.cb.call(this.scope)),this}copyFrom(t){return(this._x!==t.x||this._y!==t.y)&&(this._x=t.x,this._y=t.y,this.cb.call(this.scope)),this}copyTo(t){return t.set(this._x,this._y),t}equals(t){return t.x===this._x&&t.y===this._y}get x(){return this._x}set x(t){this._x!==t&&(this._x=t,this.cb.call(this.scope))}get y(){return this._y}set y(t){this._y!==t&&(this._y=t,this.cb.call(this.scope))}}Fs.prototype.toString=function(){return`[@pixi/math:ObservablePoint x=${this.x} y=${this.y} scope=${this.scope}]`};const Cm=class{constructor(){this.worldTransform=new qe,this.localTransform=new qe,this.position=new Fs(this.onChange,this,0,0),this.scale=new Fs(this.onChange,this,1,1),this.pivot=new Fs(this.onChange,this,0,0),this.skew=new Fs(this.updateSkew,this,0,0),this._rotation=0,this._cx=1,this._sx=0,this._cy=0,this._sy=1,this._localID=0,this._currentLocalID=0,this._worldID=0,this._parentID=0}onChange(){this._localID++}updateSkew(){this._cx=Math.cos(this._rotation+this.skew.y),this._sx=Math.sin(this._rotation+this.skew.y),this._cy=-Math.sin(this._rotation-this.skew.x),this._sy=Math.cos(this._rotation-this.skew.x),this._localID++}updateLocalTransform(){const n=this.localTransform;this._localID!==this._currentLocalID&&(n.a=this._cx*this.scale.x,n.b=this._sx*this.scale.x,n.c=this._cy*this.scale.y,n.d=this._sy*this.scale.y,n.tx=this.position.x-(this.pivot.x*n.a+this.pivot.y*n.c),n.ty=this.position.y-(this.pivot.x*n.b+this.pivot.y*n.d),this._currentLocalID=this._localID,this._parentID=-1)}updateTransform(n){const t=this.localTransform;if(this._localID!==this._currentLocalID&&(t.a=this._cx*this.scale.x,t.b=this._sx*this.scale.x,t.c=this._cy*this.scale.y,t.d=this._sy*this.scale.y,t.tx=this.position.x-(this.pivot.x*t.a+this.pivot.y*t.c),t.ty=this.position.y-(this.pivot.x*t.b+this.pivot.y*t.d),this._currentLocalID=this._localID,this._parentID=-1),this._parentID!==n._worldID){const e=n.worldTransform,i=this.worldTransform;i.a=t.a*e.a+t.b*e.c,i.b=t.a*e.b+t.b*e.d,i.c=t.c*e.a+t.d*e.c,i.d=t.c*e.b+t.d*e.d,i.tx=t.tx*e.a+t.ty*e.c+e.tx,i.ty=t.tx*e.b+t.ty*e.d+e.ty,this._parentID=n._worldID,this._worldID++}}setFromMatrix(n){n.decompose(this),this._localID++}get rotation(){return this._rotation}set rotation(n){this._rotation!==n&&(this._rotation=n,this.updateSkew())}};Cm.IDENTITY=new Cm;let M2=Cm;M2.prototype.toString=function(){return`[@pixi/math:Transform position=(${this.position.x}, ${this.position.y}) rotation=${this.rotation} scale=(${this.scale.x}, ${this.scale.y}) skew=(${this.skew.x}, ${this.skew.y}) ]`};var GR=`varying vec2 vTextureCoord; + +uniform sampler2D uSampler; + +void main(void){ + gl_FragColor *= texture2D(uSampler, vTextureCoord); +}`,jR=`attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; + +uniform mat3 projectionMatrix; + +varying vec2 vTextureCoord; + +void main(void){ + gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + vTextureCoord = aTextureCoord; +} +`;function i8(n,t,e){const i=n.createShader(t);return n.shaderSource(i,e),n.compileShader(i),i}function E0(n){const t=new Array(n);for(let e=0;en.type==="float"&&n.size===1&&!n.isArray,code:n=>` + if(uv["${n}"] !== ud["${n}"].value) + { + ud["${n}"].value = uv["${n}"] + gl.uniform1f(ud["${n}"].location, uv["${n}"]) + } + `},{test:(n,t)=>(n.type==="sampler2D"||n.type==="samplerCube"||n.type==="sampler2DArray")&&n.size===1&&!n.isArray&&(t==null||t.castToBaseTexture!==void 0),code:n=>`t = syncData.textureCount++; + + renderer.texture.bind(uv["${n}"], t); + + if(ud["${n}"].value !== t) + { + ud["${n}"].value = t; + gl.uniform1i(ud["${n}"].location, t); +; // eslint-disable-line max-len + }`},{test:(n,t)=>n.type==="mat3"&&n.size===1&&!n.isArray&&t.a!==void 0,code:n=>` + gl.uniformMatrix3fv(ud["${n}"].location, false, uv["${n}"].toArray(true)); + `,codeUbo:n=>` + var ${n}_matrix = uv.${n}.toArray(true); + + data[offset] = ${n}_matrix[0]; + data[offset+1] = ${n}_matrix[1]; + data[offset+2] = ${n}_matrix[2]; + + data[offset + 4] = ${n}_matrix[3]; + data[offset + 5] = ${n}_matrix[4]; + data[offset + 6] = ${n}_matrix[5]; + + data[offset + 8] = ${n}_matrix[6]; + data[offset + 9] = ${n}_matrix[7]; + data[offset + 10] = ${n}_matrix[8]; + `},{test:(n,t)=>n.type==="vec2"&&n.size===1&&!n.isArray&&t.x!==void 0,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v.x || cv[1] !== v.y) + { + cv[0] = v.x; + cv[1] = v.y; + gl.uniform2f(ud["${n}"].location, v.x, v.y); + }`,codeUbo:n=>` + v = uv.${n}; + + data[offset] = v.x; + data[offset+1] = v.y; + `},{test:n=>n.type==="vec2"&&n.size===1&&!n.isArray,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v[0] || cv[1] !== v[1]) + { + cv[0] = v[0]; + cv[1] = v[1]; + gl.uniform2f(ud["${n}"].location, v[0], v[1]); + } + `},{test:(n,t)=>n.type==="vec4"&&n.size===1&&!n.isArray&&t.width!==void 0,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v.x || cv[1] !== v.y || cv[2] !== v.width || cv[3] !== v.height) + { + cv[0] = v.x; + cv[1] = v.y; + cv[2] = v.width; + cv[3] = v.height; + gl.uniform4f(ud["${n}"].location, v.x, v.y, v.width, v.height) + }`,codeUbo:n=>` + v = uv.${n}; + + data[offset] = v.x; + data[offset+1] = v.y; + data[offset+2] = v.width; + data[offset+3] = v.height; + `},{test:(n,t)=>n.type==="vec4"&&n.size===1&&!n.isArray&&t.red!==void 0,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v.red || cv[1] !== v.green || cv[2] !== v.blue || cv[3] !== v.alpha) + { + cv[0] = v.red; + cv[1] = v.green; + cv[2] = v.blue; + cv[3] = v.alpha; + gl.uniform4f(ud["${n}"].location, v.red, v.green, v.blue, v.alpha) + }`,codeUbo:n=>` + v = uv.${n}; + + data[offset] = v.red; + data[offset+1] = v.green; + data[offset+2] = v.blue; + data[offset+3] = v.alpha; + `},{test:(n,t)=>n.type==="vec3"&&n.size===1&&!n.isArray&&t.red!==void 0,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v.red || cv[1] !== v.green || cv[2] !== v.blue || cv[3] !== v.a) + { + cv[0] = v.red; + cv[1] = v.green; + cv[2] = v.blue; + + gl.uniform3f(ud["${n}"].location, v.red, v.green, v.blue) + }`,codeUbo:n=>` + v = uv.${n}; + + data[offset] = v.red; + data[offset+1] = v.green; + data[offset+2] = v.blue; + `},{test:n=>n.type==="vec4"&&n.size===1&&!n.isArray,code:n=>` + cv = ud["${n}"].value; + v = uv["${n}"]; + + if(cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + + gl.uniform4f(ud["${n}"].location, v[0], v[1], v[2], v[3]) + }`}],HR={float:` + if (cv !== v) + { + cu.value = v; + gl.uniform1f(location, v); + }`,vec2:` + if (cv[0] !== v[0] || cv[1] !== v[1]) + { + cv[0] = v[0]; + cv[1] = v[1]; + + gl.uniform2f(location, v[0], v[1]) + }`,vec3:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + + gl.uniform3f(location, v[0], v[1], v[2]) + }`,vec4:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + + gl.uniform4f(location, v[0], v[1], v[2], v[3]); + }`,int:` + if (cv !== v) + { + cu.value = v; + + gl.uniform1i(location, v); + }`,ivec2:` + if (cv[0] !== v[0] || cv[1] !== v[1]) + { + cv[0] = v[0]; + cv[1] = v[1]; + + gl.uniform2i(location, v[0], v[1]); + }`,ivec3:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + + gl.uniform3i(location, v[0], v[1], v[2]); + }`,ivec4:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + + gl.uniform4i(location, v[0], v[1], v[2], v[3]); + }`,uint:` + if (cv !== v) + { + cu.value = v; + + gl.uniform1ui(location, v); + }`,uvec2:` + if (cv[0] !== v[0] || cv[1] !== v[1]) + { + cv[0] = v[0]; + cv[1] = v[1]; + + gl.uniform2ui(location, v[0], v[1]); + }`,uvec3:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + + gl.uniform3ui(location, v[0], v[1], v[2]); + }`,uvec4:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + + gl.uniform4ui(location, v[0], v[1], v[2], v[3]); + }`,bool:` + if (cv !== v) + { + cu.value = v; + gl.uniform1i(location, v); + }`,bvec2:` + if (cv[0] != v[0] || cv[1] != v[1]) + { + cv[0] = v[0]; + cv[1] = v[1]; + + gl.uniform2i(location, v[0], v[1]); + }`,bvec3:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + + gl.uniform3i(location, v[0], v[1], v[2]); + }`,bvec4:` + if (cv[0] !== v[0] || cv[1] !== v[1] || cv[2] !== v[2] || cv[3] !== v[3]) + { + cv[0] = v[0]; + cv[1] = v[1]; + cv[2] = v[2]; + cv[3] = v[3]; + + gl.uniform4i(location, v[0], v[1], v[2], v[3]); + }`,mat2:"gl.uniformMatrix2fv(location, false, v)",mat3:"gl.uniformMatrix3fv(location, false, v)",mat4:"gl.uniformMatrix4fv(location, false, v)",sampler2D:` + if (cv !== v) + { + cu.value = v; + + gl.uniform1i(location, v); + }`,samplerCube:` + if (cv !== v) + { + cu.value = v; + + gl.uniform1i(location, v); + }`,sampler2DArray:` + if (cv !== v) + { + cu.value = v; + + gl.uniform1i(location, v); + }`},qR={float:"gl.uniform1fv(location, v)",vec2:"gl.uniform2fv(location, v)",vec3:"gl.uniform3fv(location, v)",vec4:"gl.uniform4fv(location, v)",mat4:"gl.uniformMatrix4fv(location, false, v)",mat3:"gl.uniformMatrix3fv(location, false, v)",mat2:"gl.uniformMatrix2fv(location, false, v)",int:"gl.uniform1iv(location, v)",ivec2:"gl.uniform2iv(location, v)",ivec3:"gl.uniform3iv(location, v)",ivec4:"gl.uniform4iv(location, v)",uint:"gl.uniform1uiv(location, v)",uvec2:"gl.uniform2uiv(location, v)",uvec3:"gl.uniform3uiv(location, v)",uvec4:"gl.uniform4uiv(location, v)",bool:"gl.uniform1iv(location, v)",bvec2:"gl.uniform2iv(location, v)",bvec3:"gl.uniform3iv(location, v)",bvec4:"gl.uniform4iv(location, v)",sampler2D:"gl.uniform1iv(location, v)",samplerCube:"gl.uniform1iv(location, v)",sampler2DArray:"gl.uniform1iv(location, v)"};function XR(n,t){const e=[` + var v = null; + var cv = null; + var cu = null; + var t = 0; + var gl = renderer.gl; + `];for(const i in n.uniforms){const a=t[i];if(!a){n.uniforms[i]?.group===!0&&(n.uniforms[i].ubo?e.push(` + renderer.shader.syncUniformBufferGroup(uv.${i}, '${i}'); + `):e.push(` + renderer.shader.syncUniformGroup(uv.${i}, syncData); + `));continue}const o=n.uniforms[i];let c=!1;for(let u=0;u=Br.WEBGL2&&(t=n.getContext("webgl2",{})),t||(t=n.getContext("webgl",{})||n.getContext("experimental-webgl",{}),t?t.getExtension("WEBGL_draw_buffers"):t=null),Su=t}return Su}let _u;function $R(){if(!_u){_u=Yn.MEDIUM;const n=YR();if(n&&n.getShaderPrecisionFormat){const t=n.getShaderPrecisionFormat(n.FRAGMENT_SHADER,n.HIGH_FLOAT);t&&(_u=t.precision?Yn.HIGH:Yn.MEDIUM)}}return _u}function s8(n,t){const e=n.getShaderSource(t).split(` +`).map((d,m)=>`${m}: ${d}`),i=n.getShaderInfoLog(t),a=i.split(` +`),o={},c=a.map(d=>parseFloat(d.replace(/^ERROR\: 0\:([\d]+)\:.*$/,"$1"))).filter(d=>d&&!o[d]?(o[d]=!0,!0):!1),u=[""];c.forEach(d=>{e[d-1]=`%c${e[d-1]}%c`,u.push("background: #FF0000; color:#FFFFFF; font-size: 10px","font-size: 10px")});const f=e.join(` +`);u[0]=f,console.error(i),console.groupCollapsed("click to view full shader code"),console.warn(...u),console.groupEnd()}function WR(n,t,e,i){n.getProgramParameter(t,n.LINK_STATUS)||(n.getShaderParameter(e,n.COMPILE_STATUS)||s8(n,e),n.getShaderParameter(i,n.COMPILE_STATUS)||s8(n,i),console.error("PixiJS Error: Could not initialize shader."),n.getProgramInfoLog(t)!==""&&console.warn("PixiJS Warning: gl.getProgramInfoLog()",n.getProgramInfoLog(t)))}const ZR={float:1,vec2:2,vec3:3,vec4:4,int:1,ivec2:2,ivec3:3,ivec4:4,uint:1,uvec2:2,uvec3:3,uvec4:4,bool:1,bvec2:2,bvec3:3,bvec4:4,mat2:4,mat3:9,mat4:16,sampler2D:1};function dv(n){return ZR[n]}let Au=null;const r8={FLOAT:"float",FLOAT_VEC2:"vec2",FLOAT_VEC3:"vec3",FLOAT_VEC4:"vec4",INT:"int",INT_VEC2:"ivec2",INT_VEC3:"ivec3",INT_VEC4:"ivec4",UNSIGNED_INT:"uint",UNSIGNED_INT_VEC2:"uvec2",UNSIGNED_INT_VEC3:"uvec3",UNSIGNED_INT_VEC4:"uvec4",BOOL:"bool",BOOL_VEC2:"bvec2",BOOL_VEC3:"bvec3",BOOL_VEC4:"bvec4",FLOAT_MAT2:"mat2",FLOAT_MAT3:"mat3",FLOAT_MAT4:"mat4",SAMPLER_2D:"sampler2D",INT_SAMPLER_2D:"sampler2D",UNSIGNED_INT_SAMPLER_2D:"sampler2D",SAMPLER_CUBE:"samplerCube",INT_SAMPLER_CUBE:"samplerCube",UNSIGNED_INT_SAMPLER_CUBE:"samplerCube",SAMPLER_2D_ARRAY:"sampler2DArray",INT_SAMPLER_2D_ARRAY:"sampler2DArray",UNSIGNED_INT_SAMPLER_2D_ARRAY:"sampler2DArray"};function mv(n,t){if(!Au){const e=Object.keys(r8);Au={};for(let i=0;i0&&(e+=` +else `),ithis.size&&this.flush(),this._vertexCount+=t.vertexData.length/2,this._indexCount+=t.indices.length,this._bufferedTextures[this._bufferSize]=t._texture.baseTexture,this._bufferedElements[this._bufferSize++]=t)}buildTexturesAndDrawCalls(){const{_bufferedTextures:t,maxTextures:e}=this,i=ai._textureArrayPool,a=this.renderer.batch,o=this._tempBoundTextures,c=this.renderer.textureGC.count;let u=++ge._globalBatch,f=0,d=i[0],m=0;a.copyBoundTextures(o,e);for(let g=0;g=e&&(a.boundArray(d,o,u,e),this.buildDrawCalls(d,m,g),m=g,d=i[++f],++u),v._batchEnabled=u,v.touched=c,d.elements[d.count++]=v)}d.count>0&&(a.boundArray(d,o,u,e),this.buildDrawCalls(d,m,this._bufferSize),++f,++u);for(let g=0;g0);for(let C=0;C=0;--a)t[a]=i[a]||null,t[a]&&(t[a]._batchLocation=a)}boundArray(t,e,i,a){const{elements:o,ids:c,count:u}=t;let f=0;for(let d=0;d=0&&g=Br.WEBGL2&&(i=t.getContext("webgl2",e)),i)this.webGLVersion=2;else if(this.webGLVersion=1,i=t.getContext("webgl",e)||t.getContext("experimental-webgl",e),!i)throw new Error("This browser does not support WebGL. Try using the canvas renderer");return this.gl=i,this.getExtensions(),this.gl}getExtensions(){const{gl:t}=this,e={loseContext:t.getExtension("WEBGL_lose_context"),anisotropicFiltering:t.getExtension("EXT_texture_filter_anisotropic"),floatTextureLinear:t.getExtension("OES_texture_float_linear"),s3tc:t.getExtension("WEBGL_compressed_texture_s3tc"),s3tc_sRGB:t.getExtension("WEBGL_compressed_texture_s3tc_srgb"),etc:t.getExtension("WEBGL_compressed_texture_etc"),etc1:t.getExtension("WEBGL_compressed_texture_etc1"),pvrtc:t.getExtension("WEBGL_compressed_texture_pvrtc")||t.getExtension("WEBKIT_WEBGL_compressed_texture_pvrtc"),atc:t.getExtension("WEBGL_compressed_texture_atc"),astc:t.getExtension("WEBGL_compressed_texture_astc"),bptc:t.getExtension("EXT_texture_compression_bptc")};this.webGLVersion===1?Object.assign(this.extensions,e,{drawBuffers:t.getExtension("WEBGL_draw_buffers"),depthTexture:t.getExtension("WEBGL_depth_texture"),vertexArrayObject:t.getExtension("OES_vertex_array_object")||t.getExtension("MOZ_OES_vertex_array_object")||t.getExtension("WEBKIT_OES_vertex_array_object"),uint32ElementIndex:t.getExtension("OES_element_index_uint"),floatTexture:t.getExtension("OES_texture_float"),floatTextureLinear:t.getExtension("OES_texture_float_linear"),textureHalfFloat:t.getExtension("OES_texture_half_float"),textureHalfFloatLinear:t.getExtension("OES_texture_half_float_linear")}):this.webGLVersion===2&&Object.assign(this.extensions,e,{colorBufferFloat:t.getExtension("EXT_color_buffer_float")})}handleContextLost(t){t.preventDefault(),setTimeout(()=>{this.gl.isContextLost()&&this.extensions.loseContext&&this.extensions.loseContext.restoreContext()},0)}handleContextRestored(){this.renderer.runners.contextChange.emit(this.gl)}destroy(){const t=this.renderer.view;this.renderer=null,t.removeEventListener!==void 0&&(t.removeEventListener("webglcontextlost",this.handleContextLost),t.removeEventListener("webglcontextrestored",this.handleContextRestored)),this.gl.useProgram(null),this.extensions.loseContext&&this.extensions.loseContext.loseContext()}postrender(){this.renderer.objectRenderer.renderingToScreen&&this.gl.flush()}validateContext(t){const e=t.getContextAttributes(),i="WebGL2RenderingContext"in globalThis&&t instanceof globalThis.WebGL2RenderingContext;i&&(this.webGLVersion=2),e&&!e.stencil&&console.warn("Provided WebGL context does not have a stencil buffer, masks may not render correctly");const a=i||!!t.getExtension("OES_element_index_uint");this.supports.uint32Indices=a,a||console.warn("Provided WebGL context does not support 32 index buffer, complex graphics may not render correctly")}}fh.defaultOptions={context:null,antialias:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!1,powerPreference:"default"},fh.extension={type:Ht.RendererSystem,name:"context"};ae.add(fh);class Am{constructor(t,e){if(this.width=Math.round(t),this.height=Math.round(e),!this.width||!this.height)throw new Error("Framebuffer width or height is zero");this.stencil=!1,this.depth=!1,this.dirtyId=0,this.dirtyFormat=0,this.dirtySize=0,this.depthTexture=null,this.colorTextures=[],this.glFramebuffers={},this.disposeRunner=new ui("disposeFramebuffer"),this.multisample=Ge.NONE}get colorTexture(){return this.colorTextures[0]}addColorTexture(t=0,e){return this.colorTextures[t]=e||new ge(null,{scaleMode:es.NEAREST,resolution:1,mipmap:Nr.OFF,width:this.width,height:this.height}),this.dirtyId++,this.dirtyFormat++,this}addDepthTexture(t){return this.depthTexture=t||new ge(null,{scaleMode:es.NEAREST,resolution:1,width:this.width,height:this.height,mipmap:Nr.OFF,format:gt.DEPTH_COMPONENT,type:jt.UNSIGNED_SHORT}),this.dirtyId++,this.dirtyFormat++,this}enableDepth(){return this.depth=!0,this.dirtyId++,this.dirtyFormat++,this}enableStencil(){return this.stencil=!0,this.dirtyId++,this.dirtyFormat++,this}resize(t,e){if(t=Math.round(t),e=Math.round(e),!t||!e)throw new Error("Framebuffer width and height must not be zero");if(!(t===this.width&&e===this.height)){this.width=t,this.height=e,this.dirtyId++,this.dirtySize++;for(let i=0;i{const a=this.source;this.url=a.src;const o=()=>{this.destroyed||(a.onload=null,a.onerror=null,this.update(),this._load=null,this.createBitmap?e(this.process()):e(this))};a.complete&&a.src?o():(a.onload=o,a.onerror=c=>{i(c),this.onError.emit(c)})}),this._load)}process(){const t=this.source;if(this._process!==null)return this._process;if(this.bitmap!==null||!globalThis.createImageBitmap)return Promise.resolve(this);const e=globalThis.createImageBitmap,i=!t.crossOrigin||t.crossOrigin==="anonymous";return this._process=fetch(t.src,{mode:i?"cors":"no-cors"}).then(a=>a.blob()).then(a=>e(a,0,0,t.width,t.height,{premultiplyAlpha:this.alphaMode===null||this.alphaMode===Pr.UNPACK?"premultiply":"none"})).then(a=>this.destroyed?Promise.reject():(this.bitmap=a,this.update(),this._process=null,Promise.resolve(this))),this._process}upload(t,e,i){if(typeof this.alphaMode=="number"&&(e.alphaMode=this.alphaMode),!this.createBitmap)return super.upload(t,e,i);if(!this.bitmap&&(this.process(),!this.bitmap))return!1;if(super.upload(t,e,i,this.bitmap),!this.preserveBitmap){let a=!0;const o=e._glTextures;for(const c in o){const u=o[c];if(u!==i&&u.dirtyId!==e.dirtyId){a=!1;break}}a&&(this.bitmap.close&&this.bitmap.close(),this.bitmap=null)}return!0}dispose(){this.source.onload=null,this.source.onerror=null,super.dispose(),this.bitmap&&(this.bitmap.close(),this.bitmap=null),this._process=null,this._load=null}static test(t){return typeof HTMLImageElement<"u"&&(typeof t=="string"||t instanceof HTMLImageElement)}}class R2{constructor(){this.x0=0,this.y0=0,this.x1=1,this.y1=0,this.x2=1,this.y2=1,this.x3=0,this.y3=1,this.uvsFloat32=new Float32Array(8)}set(t,e,i){const a=e.width,o=e.height;if(i){const c=t.width/2/a,u=t.height/2/o,f=t.x/a+c,d=t.y/o+u;i=_e.add(i,_e.NW),this.x0=f+c*_e.uX(i),this.y0=d+u*_e.uY(i),i=_e.add(i,2),this.x1=f+c*_e.uX(i),this.y1=d+u*_e.uY(i),i=_e.add(i,2),this.x2=f+c*_e.uX(i),this.y2=d+u*_e.uY(i),i=_e.add(i,2),this.x3=f+c*_e.uX(i),this.y3=d+u*_e.uY(i)}else this.x0=t.x/a,this.y0=t.y/o,this.x1=(t.x+t.width)/a,this.y1=t.y/o,this.x2=(t.x+t.width)/a,this.y2=(t.y+t.height)/o,this.x3=t.x/a,this.y3=(t.y+t.height)/o;this.uvsFloat32[0]=this.x0,this.uvsFloat32[1]=this.y0,this.uvsFloat32[2]=this.x1,this.uvsFloat32[3]=this.y1,this.uvsFloat32[4]=this.x2,this.uvsFloat32[5]=this.y2,this.uvsFloat32[6]=this.x3,this.uvsFloat32[7]=this.y3}}R2.prototype.toString=function(){return`[@pixi/core:TextureUvs x0=${this.x0} y0=${this.y0} x1=${this.x1} y1=${this.y1} x2=${this.x2} y2=${this.y2} x3=${this.x3} y3=${this.y3}]`};const l8=new R2;function wu(n){n.destroy=function(){},n.on=function(){},n.once=function(){},n.emit=function(){}}class ne extends Eh{constructor(t,e,i,a,o,c,u){if(super(),this.noFrame=!1,e||(this.noFrame=!0,e=new be(0,0,1,1)),t instanceof ne&&(t=t.baseTexture),this.baseTexture=t,this._frame=e,this.trim=a,this.valid=!1,this.destroyed=!1,this._uvs=l8,this.uvMatrix=null,this.orig=i||e,this._rotate=Number(o||0),o===!0)this._rotate=2;else if(this._rotate%2!==0)throw new Error("attempt to use diamond-shaped UVs. If you are sure, set rotation manually");this.defaultAnchor=c?new an(c.x,c.y):new an(0,0),this.defaultBorders=u,this._updateID=0,this.textureCacheIds=[],t.valid?this.noFrame?t.valid&&this.onBaseTextureUpdated(t):this.frame=e:t.once("loaded",this.onBaseTextureUpdated,this),this.noFrame&&t.on("update",this.onBaseTextureUpdated,this)}update(){this.baseTexture.resource&&this.baseTexture.resource.update()}onBaseTextureUpdated(t){if(this.noFrame){if(!this.baseTexture.valid)return;this._frame.width=t.width,this._frame.height=t.height,this.valid=!0,this.updateUvs()}else this.frame=this._frame;this.emit("update",this)}destroy(t){if(this.baseTexture){if(t){const{resource:e}=this.baseTexture;e?.url&&bi[e.url]&&ne.removeFromCache(e.url),this.baseTexture.destroy()}this.baseTexture.off("loaded",this.onBaseTextureUpdated,this),this.baseTexture.off("update",this.onBaseTextureUpdated,this),this.baseTexture=null}this._frame=null,this._uvs=null,this.trim=null,this.orig=null,this.valid=!1,ne.removeFromCache(this),this.textureCacheIds=null,this.destroyed=!0,this.emit("destroyed",this),this.removeAllListeners()}clone(){const t=this._frame.clone(),e=this._frame===this.orig?t:this.orig.clone(),i=new ne(this.baseTexture,!this.noFrame&&t,e,this.trim?.clone(),this.rotate,this.defaultAnchor,this.defaultBorders);return this.noFrame&&(i._frame=t),i}updateUvs(){this._uvs===l8&&(this._uvs=new R2),this._uvs.set(this._frame,this.baseTexture,this.rotate),this._updateID++}static from(t,e={},i=Kt.STRICT_TEXTURE_CACHE){const a=typeof t=="string";let o=null;if(a)o=t;else if(t instanceof ge){if(!t.cacheId){const u=e?.pixiIdPrefix||"pixiid";t.cacheId=`${u}-${Ul()}`,ge.addToCache(t,t.cacheId)}o=t.cacheId}else{if(!t._pixiId){const u=e?.pixiIdPrefix||"pixiid";t._pixiId=`${u}_${Ul()}`}o=t._pixiId}let c=bi[o];if(a&&i&&!c)throw new Error(`The cacheId "${o}" does not exist in TextureCache.`);return!c&&!(t instanceof ge)?(e.resolution||(e.resolution=t8(t)),c=new ne(new ge(t,e)),c.baseTexture.cacheId=o,ge.addToCache(c.baseTexture,o),ne.addToCache(c,o)):!c&&t instanceof ge&&(c=new ne(t),ne.addToCache(c,o)),c}static fromURL(t,e){const i=Object.assign({autoLoad:!1},e?.resourceOptions),a=ne.from(t,Object.assign({resourceOptions:i},e),!1),o=a.baseTexture.resource;return a.baseTexture.valid?Promise.resolve(a):o.load().then(()=>Promise.resolve(a))}static fromBuffer(t,e,i,a){return new ne(ge.fromBuffer(t,e,i,a))}static fromLoader(t,e,i,a){const o=new ge(t,Object.assign({scaleMode:ge.defaultOptions.scaleMode,resolution:t8(e)},a)),{resource:c}=o;c instanceof vv&&(c.url=e);const u=new ne(o);return i||(i=e),ge.addToCache(u.baseTexture,i),ne.addToCache(u,i),i!==e&&(ge.addToCache(u.baseTexture,e),ne.addToCache(u,e)),u.baseTexture.valid?Promise.resolve(u):new Promise(f=>{u.baseTexture.once("loaded",()=>f(u))})}static addToCache(t,e){e&&(t.textureCacheIds.includes(e)||t.textureCacheIds.push(e),bi[e]&&bi[e]!==t&&console.warn(`Texture added to the cache with an id [${e}] that already had an entry`),bi[e]=t)}static removeFromCache(t){if(typeof t=="string"){const e=bi[t];if(e){const i=e.textureCacheIds.indexOf(t);return i>-1&&e.textureCacheIds.splice(i,1),delete bi[t],e}}else if(t?.textureCacheIds){for(let e=0;ethis.baseTexture.width,u=i+o>this.baseTexture.height;if(c||u){const f=c&&u?"and":"or",d=`X: ${e} + ${a} = ${e+a} > ${this.baseTexture.width}`,m=`Y: ${i} + ${o} = ${i+o} > ${this.baseTexture.height}`;throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions: ${d} ${f} ${m}`)}this.valid=a&&o&&this.baseTexture.valid,!this.trim&&!this.rotate&&(this.orig=t),this.valid&&this.updateUvs()}get rotate(){return this._rotate}set rotate(t){this._rotate=t,this.valid&&this.updateUvs()}get width(){return this.orig.width}get height(){return this.orig.height}castToBaseTexture(){return this.baseTexture}static get EMPTY(){return ne._EMPTY||(ne._EMPTY=new ne(new ge),wu(ne._EMPTY),wu(ne._EMPTY.baseTexture)),ne._EMPTY}static get WHITE(){if(!ne._WHITE){const t=Kt.ADAPTER.createCanvas(16,16),e=t.getContext("2d");t.width=16,t.height=16,e.fillStyle="white",e.fillRect(0,0,16,16),ne._WHITE=new ne(ge.from(t)),wu(ne._WHITE),wu(ne._WHITE.baseTexture)}return ne._WHITE}}class Lh extends ne{constructor(t,e){super(t,e),this.valid=!0,this.filterFrame=null,this.filterPoolKey=null,this.updateUvs()}get framebuffer(){return this.baseTexture.framebuffer}get multisample(){return this.framebuffer.multisample}set multisample(t){this.framebuffer.multisample=t}resize(t,e,i=!0){const a=this.baseTexture.resolution,o=Math.round(t*a)/a,c=Math.round(e*a)/a;this.valid=o>0&&c>0,this._frame.width=this.orig.width=o,this._frame.height=this.orig.height=c,i&&this.baseTexture.resize(o,c),this.updateUvs()}setResolution(t){const{baseTexture:e}=this;e.resolution!==t&&(e.setResolution(t),this.resize(e.width,e.height,!1))}static create(t){return new Lh(new yv(t))}}class bv{constructor(t){this.texturePool={},this.textureOptions=t||{},this.enableFullScreen=!1,this._pixelsWidth=0,this._pixelsHeight=0}createTexture(t,e,i=Ge.NONE){const a=new yv(Object.assign({width:t,height:e,resolution:1,multisample:i},this.textureOptions));return new Lh(a)}getOptimalTexture(t,e,i=1,a=Ge.NONE){let o;t=Math.max(Math.ceil(t*i-1e-6),1),e=Math.max(Math.ceil(e*i-1e-6),1),!this.enableFullScreen||t!==this._pixelsWidth||e!==this._pixelsHeight?(t=ch(t),e=ch(e),o=((t&65535)<<16|e&65535)>>>0,a>1&&(o+=a*4294967296)):o=a>1?-a:-1,this.texturePool[o]||(this.texturePool[o]=[]);let c=this.texturePool[o].pop();return c||(c=this.createTexture(t,e,a)),c.filterPoolKey=o,c.setResolution(i),c}getFilterTexture(t,e,i){const a=this.getOptimalTexture(t.width,t.height,e||t.resolution,i||Ge.NONE);return a.filterFrame=t.filterFrame,a}returnTexture(t){const e=t.filterPoolKey;t.filterFrame=null,this.texturePool[e].push(t)}returnFilterTexture(t){this.returnTexture(t)}clear(t){if(t=t!==!1,t)for(const e in this.texturePool){const i=this.texturePool[e];if(i)for(let a=0;a0&&t.height>0;for(const e in this.texturePool){if(!(Number(e)<0))continue;const i=this.texturePool[e];if(i)for(let a=0;a1&&(m=this.getOptimalFilterTexture(d.width,d.height,e.resolution),m.filterFrame=d.filterFrame),i[g].apply(this,d,m,Ci.CLEAR,e);const v=d;d=m,m=v}i[g].apply(this,d,f.renderTexture,Ci.BLEND,e),g>1&&e.multisample>1&&this.returnFilterTexture(e.renderTexture),this.returnFilterTexture(d),this.returnFilterTexture(m)}e.clear(),this.statePool.push(e)}bindAndClear(t,e=Ci.CLEAR){const{renderTexture:i,state:a}=this.renderer;if(t===this.defaultFilterStack[this.defaultFilterStack.length-1].renderTexture?this.renderer.projection.transform=this.activeState.transform:this.renderer.projection.transform=null,t?.filterFrame){const c=this.tempRect;c.x=0,c.y=0,c.width=t.filterFrame.width,c.height=t.filterFrame.height,i.bind(t,t.filterFrame,c)}else t!==this.defaultFilterStack[this.defaultFilterStack.length-1].renderTexture?i.bind(t):this.renderer.renderTexture.bind(t,this.activeState.bindingSourceFrame,this.activeState.bindingDestinationFrame);const o=a.stateId&1||this.forceClear;(e===Ci.CLEAR||e===Ci.BLIT&&o)&&this.renderer.framebuffer.clear(0,0,0,0)}applyFilter(t,e,i,a){const o=this.renderer;o.state.set(t.state),this.bindAndClear(i,a),t.uniforms.uSampler=e,t.uniforms.filterGlobals=this.globalUniforms,o.shader.bind(t),t.legacy=!!t.program.attributeData.aTextureCoord,t.legacy?(this.quadUv.map(e._frame,e.filterFrame),o.geometry.bind(this.quadUv),o.geometry.draw(lh.TRIANGLES)):(o.geometry.bind(this.quad),o.geometry.draw(lh.TRIANGLE_STRIP))}calculateSpriteMatrix(t,e){const{sourceFrame:i,destinationFrame:a}=this.activeState,{orig:o}=e._texture,c=t.set(a.width,0,0,a.height,i.x,i.y),u=e.worldTransform.copyTo(qe.TEMP_MATRIX);return u.invert(),c.prepend(u),c.scale(1/o.width,1/o.height),c.translate(e.anchor.x,e.anchor.y),c}destroy(){this.renderer=null,this.texturePool.clear(!1)}getOptimalFilterTexture(t,e,i=1,a=Ge.NONE){return this.texturePool.getOptimalTexture(t,e,i,a)}getFilterTexture(t,e,i){if(typeof t=="number"){const o=t;t=e,e=o}t=t||this.activeState.renderTexture;const a=this.texturePool.getOptimalTexture(t.width,t.height,e||t.resolution,i||Ge.NONE);return a.filterFrame=t.filterFrame,a}returnFilterTexture(t){this.texturePool.returnTexture(t)}emptyPool(){this.texturePool.clear(!0)}resize(){this.texturePool.setScreenSize(this.renderer.view)}transformAABB(t,e){const i=Mu[0],a=Mu[1],o=Mu[2],c=Mu[3];i.set(e.left,e.top),a.set(e.left,e.bottom),o.set(e.right,e.top),c.set(e.right,e.bottom),t.apply(i,i),t.apply(a,a),t.apply(o,o),t.apply(c,c);const u=Math.min(i.x,a.x,o.x,c.x),f=Math.min(i.y,a.y,o.y,c.y),d=Math.max(i.x,a.x,o.x,c.x),m=Math.max(i.y,a.y,o.y,c.y);e.x=u,e.y=f,e.width=d-u,e.height=m-f}roundFrame(t,e,i,a,o){if(!(t.width<=0||t.height<=0||i.width<=0||i.height<=0)){if(o){const{a:c,b:u,c:f,d}=o;if((Math.abs(u)>1e-4||Math.abs(f)>1e-4)&&(Math.abs(c)>1e-4||Math.abs(d)>1e-4))return}o=o?w0.copyFrom(o):w0.identity(),o.translate(-i.x,-i.y).scale(a.width/i.width,a.height/i.height).translate(a.x,a.y),this.transformAABB(o,t),t.ceil(e),this.transformAABB(o.invert(),t)}}}xv.extension={type:Ht.RendererSystem,name:"filter"};ae.add(xv);class hL{constructor(t){this.framebuffer=t,this.stencil=null,this.dirtyId=-1,this.dirtyFormat=-1,this.dirtySize=-1,this.multisample=Ge.NONE,this.msaaBuffer=null,this.blitFramebuffer=null,this.mipLevel=0}}const fL=new be;class Tv{constructor(t){this.renderer=t,this.managedFramebuffers=[],this.unknownFramebuffer=new Am(10,10),this.msaaSamples=null}contextChange(){this.disposeAll(!0);const t=this.gl=this.renderer.gl;if(this.CONTEXT_UID=this.renderer.CONTEXT_UID,this.current=this.unknownFramebuffer,this.viewport=new be,this.hasMRT=!0,this.writeDepthTexture=!0,this.renderer.context.webGLVersion===1){let e=this.renderer.context.extensions.drawBuffers,i=this.renderer.context.extensions.depthTexture;Kt.PREFER_ENV===Br.WEBGL_LEGACY&&(e=null,i=null),e?t.drawBuffers=a=>e.drawBuffersWEBGL(a):(this.hasMRT=!1,t.drawBuffers=()=>{}),i||(this.writeDepthTexture=!1)}else this.msaaSamples=t.getInternalformatParameter(t.RENDERBUFFER,t.RGBA8,t.SAMPLES)}bind(t,e,i=0){const{gl:a}=this;if(t){const o=t.glFramebuffers[this.CONTEXT_UID]||this.initFramebuffer(t);this.current!==t&&(this.current=t,a.bindFramebuffer(a.FRAMEBUFFER,o.framebuffer)),o.mipLevel!==i&&(t.dirtyId++,t.dirtyFormat++,o.mipLevel=i),o.dirtyId!==t.dirtyId&&(o.dirtyId=t.dirtyId,o.dirtyFormat!==t.dirtyFormat?(o.dirtyFormat=t.dirtyFormat,o.dirtySize=t.dirtySize,this.updateFramebuffer(t,i)):o.dirtySize!==t.dirtySize&&(o.dirtySize=t.dirtySize,this.resizeFramebuffer(t)));for(let c=0;c>i,u=e.height>>i,f=c/e.width;this.setViewport(e.x*f,e.y*f,c,u)}else{const c=t.width>>i,u=t.height>>i;this.setViewport(0,0,c,u)}}else this.current&&(this.current=null,a.bindFramebuffer(a.FRAMEBUFFER,null)),e?this.setViewport(e.x,e.y,e.width,e.height):this.setViewport(0,0,this.renderer.width,this.renderer.height)}setViewport(t,e,i,a){const o=this.viewport;t=Math.round(t),e=Math.round(e),i=Math.round(i),a=Math.round(a),(o.width!==i||o.height!==a||o.x!==t||o.y!==e)&&(o.x=t,o.y=e,o.width=i,o.height=a,this.gl.viewport(t,e,i,a))}get size(){return this.current?{x:0,y:0,width:this.current.width,height:this.current.height}:{x:0,y:0,width:this.renderer.width,height:this.renderer.height}}clear(t,e,i,a,o=pm.COLOR|pm.DEPTH){const{gl:c}=this;c.clearColor(t,e,i,a),c.clear(o)}initFramebuffer(t){const{gl:e}=this,i=new hL(e.createFramebuffer());return i.multisample=this.detectSamples(t.multisample),t.glFramebuffers[this.CONTEXT_UID]=i,this.managedFramebuffers.push(t),t.disposeRunner.add(this),i}resizeFramebuffer(t){const{gl:e}=this,i=t.glFramebuffers[this.CONTEXT_UID];if(i.stencil){e.bindRenderbuffer(e.RENDERBUFFER,i.stencil);let c;this.renderer.context.webGLVersion===1?c=e.DEPTH_STENCIL:t.depth&&t.stencil?c=e.DEPTH24_STENCIL8:t.depth?c=e.DEPTH_COMPONENT24:c=e.STENCIL_INDEX8,i.msaaBuffer?e.renderbufferStorageMultisample(e.RENDERBUFFER,i.multisample,c,t.width,t.height):e.renderbufferStorage(e.RENDERBUFFER,c,t.width,t.height)}const a=t.colorTextures;let o=a.length;e.drawBuffers||(o=Math.min(o,1));for(let c=0;c1&&this.canMultisampleFramebuffer(t)?a.msaaBuffer=a.msaaBuffer||i.createRenderbuffer():a.msaaBuffer&&(i.deleteRenderbuffer(a.msaaBuffer),a.msaaBuffer=null,a.blitFramebuffer&&(a.blitFramebuffer.dispose(),a.blitFramebuffer=null));const u=[];for(let f=0;f1&&i.drawBuffers(u),t.depthTexture&&this.writeDepthTexture){const f=t.depthTexture;this.renderer.texture.bind(f,0),i.framebufferTexture2D(i.FRAMEBUFFER,i.DEPTH_ATTACHMENT,i.TEXTURE_2D,f._glTextures[this.CONTEXT_UID].texture,e)}if((t.stencil||t.depth)&&!(t.depthTexture&&this.writeDepthTexture)){a.stencil=a.stencil||i.createRenderbuffer();let f,d;this.renderer.context.webGLVersion===1?(f=i.DEPTH_STENCIL_ATTACHMENT,d=i.DEPTH_STENCIL):t.depth&&t.stencil?(f=i.DEPTH_STENCIL_ATTACHMENT,d=i.DEPTH24_STENCIL8):t.depth?(f=i.DEPTH_ATTACHMENT,d=i.DEPTH_COMPONENT24):(f=i.STENCIL_ATTACHMENT,d=i.STENCIL_INDEX8),i.bindRenderbuffer(i.RENDERBUFFER,a.stencil),a.msaaBuffer?i.renderbufferStorageMultisample(i.RENDERBUFFER,a.multisample,d,t.width,t.height):i.renderbufferStorage(i.RENDERBUFFER,d,t.width,t.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,f,i.RENDERBUFFER,a.stencil)}else a.stencil&&(i.deleteRenderbuffer(a.stencil),a.stencil=null)}canMultisampleFramebuffer(t){return this.renderer.context.webGLVersion!==1&&t.colorTextures.length<=1&&!t.depthTexture}detectSamples(t){const{msaaSamples:e}=this;let i=Ge.NONE;if(t<=1||e===null)return i;for(let a=0;a=0&&this.managedFramebuffers.splice(o,1),t.disposeRunner.remove(this),e||(a.deleteFramebuffer(i.framebuffer),i.msaaBuffer&&a.deleteRenderbuffer(i.msaaBuffer),i.stencil&&a.deleteRenderbuffer(i.stencil)),i.blitFramebuffer&&this.disposeFramebuffer(i.blitFramebuffer,e)}disposeAll(t){const e=this.managedFramebuffers;this.managedFramebuffers=[];for(let i=0;ii.createVertexArrayOES(),t.bindVertexArray=a=>i.bindVertexArrayOES(a),t.deleteVertexArray=a=>i.deleteVertexArrayOES(a)):(this.hasVao=!1,t.createVertexArray=()=>null,t.bindVertexArray=()=>null,t.deleteVertexArray=()=>null)}if(e.webGLVersion!==2){const i=t.getExtension("ANGLE_instanced_arrays");i?(t.vertexAttribDivisor=(a,o)=>i.vertexAttribDivisorANGLE(a,o),t.drawElementsInstanced=(a,o,c,u,f)=>i.drawElementsInstancedANGLE(a,o,c,u,f),t.drawArraysInstanced=(a,o,c,u)=>i.drawArraysInstancedANGLE(a,o,c,u)):this.hasInstance=!1}this.canUseUInt32ElementIndex=e.webGLVersion===2||!!e.extensions.uint32ElementIndex}bind(t,e){e=e||this.renderer.shader.shader;const{gl:i}=this;let a=t.glVertexArrayObjects[this.CONTEXT_UID],o=!1;a||(this.managedGeometries[t.id]=t,t.disposeRunner.add(this),t.glVertexArrayObjects[this.CONTEXT_UID]=a={},o=!0);const c=a[e.program.id]||this.initGeometryVao(t,e,o);this._activeGeometry=t,this._activeVao!==c&&(this._activeVao=c,this.hasVao?i.bindVertexArray(c):this.activateVao(t,e.program)),this.updateBuffers()}reset(){this.unbind()}updateBuffers(){const t=this._activeGeometry,e=this.renderer.buffer;for(let i=0;i"u"?.5:e,this.isSimple=!1}get texture(){return this._texture}set texture(t){this._texture=t,this._textureID=-1}multiplyUvs(t,e){e===void 0&&(e=t);const i=this.mapCoord;for(let a=0;a0?this.maskStack[this.maskStack.length-1]._colorMask:15;i!==e&&this.renderer.gl.colorMask((i&1)!==0,(i&2)!==0,(i&4)!==0,(i&8)!==0)}destroy(){this.renderer=null}}Sv.extension={type:Ht.RendererSystem,name:"mask"};ae.add(Sv);class _v{constructor(t){this.renderer=t,this.maskStack=[],this.glConst=0}getStackLength(){return this.maskStack.length}setMaskStack(t){const{gl:e}=this.renderer,i=this.getStackLength();this.maskStack=t;const a=this.getStackLength();a!==i&&(a===0?e.disable(this.glConst):(e.enable(this.glConst),this._useCurrent()))}_useCurrent(){}destroy(){this.renderer=null,this.maskStack=null}}const u8=new qe,h8=[],Av=class Xu extends _v{constructor(t){super(t),this.glConst=Kt.ADAPTER.getWebGLRenderingContext().SCISSOR_TEST}getStackLength(){const t=this.maskStack[this.maskStack.length-1];return t?t._scissorCounter:0}calcScissorRect(t){if(t._scissorRectLocal)return;const e=t._scissorRect,{maskObject:i}=t,{renderer:a}=this,o=a.renderTexture,c=i.getBounds(!0,h8.pop()??new be);this.roundFrameToPixels(c,o.current?o.current.resolution:a.resolution,o.sourceFrame,o.destinationFrame,a.projection.transform),e&&c.fit(e),t._scissorRectLocal=c}static isMatrixRotated(t){if(!t)return!1;const{a:e,b:i,c:a,d:o}=t;return(Math.abs(i)>1e-4||Math.abs(a)>1e-4)&&(Math.abs(e)>1e-4||Math.abs(o)>1e-4)}testScissor(t){const{maskObject:e}=t;if(!e.isFastRect||!e.isFastRect()||Xu.isMatrixRotated(e.worldTransform)||Xu.isMatrixRotated(this.renderer.projection.transform))return!1;this.calcScissorRect(t);const i=t._scissorRectLocal;return i.width>0&&i.height>0}roundFrameToPixels(t,e,i,a,o){Xu.isMatrixRotated(o)||(o=o?u8.copyFrom(o):u8.identity(),o.translate(-i.x,-i.y).scale(a.width/i.width,a.height/i.height).translate(a.x,a.y),this.renderer.filter.transformAABB(o,t),t.fit(a),t.x=Math.round(t.x*e),t.y=Math.round(t.y*e),t.width=Math.round(t.width*e),t.height=Math.round(t.height*e))}push(t){t._scissorRectLocal||this.calcScissorRect(t);const{gl:e}=this.renderer;t._scissorRect||e.enable(e.SCISSOR_TEST),t._scissorCounter++,t._scissorRect=t._scissorRectLocal,this._useCurrent()}pop(t){const{gl:e}=this.renderer;t&&h8.push(t._scissorRectLocal),this.getStackLength()>0?this._useCurrent():e.disable(e.SCISSOR_TEST)}_useCurrent(){const t=this.maskStack[this.maskStack.length-1]._scissorRect;let e;this.renderer.renderTexture.current?e=t.y:e=this.renderer.height-t.height-t.y,this.renderer.gl.scissor(t.x,e,t.width,t.height)}};Av.extension={type:Ht.RendererSystem,name:"scissor"};let vL=Av;ae.add(vL);class Ev extends _v{constructor(t){super(t),this.glConst=Kt.ADAPTER.getWebGLRenderingContext().STENCIL_TEST}getStackLength(){const t=this.maskStack[this.maskStack.length-1];return t?t._stencilCounter:0}push(t){const e=t.maskObject,{gl:i}=this.renderer,a=t._stencilCounter;a===0&&(this.renderer.framebuffer.forceStencil(),i.clearStencil(0),i.clear(i.STENCIL_BUFFER_BIT),i.enable(i.STENCIL_TEST)),t._stencilCounter++;const o=t._colorMask;o!==0&&(t._colorMask=0,i.colorMask(!1,!1,!1,!1)),i.stencilFunc(i.EQUAL,a,4294967295),i.stencilOp(i.KEEP,i.KEEP,i.INCR),e.renderable=!0,e.render(this.renderer),this.renderer.batch.flush(),e.renderable=!1,o!==0&&(t._colorMask=o,i.colorMask((o&1)!==0,(o&2)!==0,(o&4)!==0,(o&8)!==0)),this._useCurrent()}pop(t){const e=this.renderer.gl;if(this.getStackLength()===0)e.disable(e.STENCIL_TEST);else{const i=this.maskStack.length!==0?this.maskStack[this.maskStack.length-1]:null,a=i?i._colorMask:15;a!==0&&(i._colorMask=0,e.colorMask(!1,!1,!1,!1)),e.stencilOp(e.KEEP,e.KEEP,e.DECR),t.renderable=!0,t.render(this.renderer),this.renderer.batch.flush(),t.renderable=!1,a!==0&&(i._colorMask=a,e.colorMask((a&1)!==0,(a&2)!==0,(a&4)!==0,(a&8)!==0)),this._useCurrent()}}_useCurrent(){const t=this.renderer.gl;t.stencilFunc(t.EQUAL,this.getStackLength(),4294967295),t.stencilOp(t.KEEP,t.KEEP,t.KEEP)}}Ev.extension={type:Ht.RendererSystem,name:"stencil"};ae.add(Ev);class wv{constructor(t){this.renderer=t,this.plugins={},Object.defineProperties(this.plugins,{extract:{enumerable:!1,get(){return he("7.0.0","renderer.plugins.extract has moved to renderer.extract"),t.extract}},prepare:{enumerable:!1,get(){return he("7.0.0","renderer.plugins.prepare has moved to renderer.prepare"),t.prepare}},interaction:{enumerable:!1,get(){return he("7.0.0","renderer.plugins.interaction has been deprecated, use renderer.events"),t.events}}})}init(){const t=this.rendererPlugins;for(const e in t)this.plugins[e]=new t[e](this.renderer)}destroy(){for(const t in this.plugins)this.plugins[t].destroy(),this.plugins[t]=null}}wv.extension={type:[Ht.RendererSystem,Ht.CanvasRendererSystem],name:"_plugin"};ae.add(wv);class Mv{constructor(t){this.renderer=t,this.destinationFrame=null,this.sourceFrame=null,this.defaultFrame=null,this.projectionMatrix=new qe,this.transform=null}update(t,e,i,a){this.destinationFrame=t||this.destinationFrame||this.defaultFrame,this.sourceFrame=e||this.sourceFrame||t,this.calculateProjection(this.destinationFrame,this.sourceFrame,i,a),this.transform&&this.projectionMatrix.append(this.transform);const o=this.renderer;o.globalUniforms.uniforms.projectionMatrix=this.projectionMatrix,o.globalUniforms.update(),o.shader.shader&&o.shader.syncUniformGroup(o.shader.shader.uniforms.globals)}calculateProjection(t,e,i,a){const o=this.projectionMatrix,c=a?-1:1;o.identity(),o.a=1/e.width*2,o.d=c*(1/e.height*2),o.tx=-1-e.x*o.a,o.ty=-c-e.y*o.d}setTransform(t){}destroy(){this.renderer=null}}Mv.extension={type:Ht.RendererSystem,name:"projection"};ae.add(Mv);const bL=new M2,f8=new be;class Rv{constructor(t){this.renderer=t,this._tempMatrix=new qe}generateTexture(t,e){const{region:i,...a}=e||{},o=i?.copyTo(f8)||t.getLocalBounds(f8,!0),c=a.resolution||this.renderer.resolution;o.width=Math.max(o.width,1/c),o.height=Math.max(o.height,1/c),a.width=o.width,a.height=o.height,a.resolution=c,a.multisample??(a.multisample=this.renderer.multisample);const u=Lh.create(a);this._tempMatrix.tx=-o.x,this._tempMatrix.ty=-o.y;const f=t.transform;return t.transform=bL,this.renderer.render(t,{renderTexture:u,transform:this._tempMatrix,skipUpdateTransform:!!t.parent,blit:!0}),t.transform=f,u}destroy(){}}Rv.extension={type:[Ht.RendererSystem,Ht.CanvasRendererSystem],name:"textureGenerator"};ae.add(Rv);const fr=new be,sl=new be;class Lv{constructor(t){this.renderer=t,this.defaultMaskStack=[],this.current=null,this.sourceFrame=new be,this.destinationFrame=new be,this.viewportFrame=new be}contextChange(){const t=this.renderer?.gl.getContextAttributes();this._rendererPremultipliedAlpha=!!(t&&t.alpha&&t.premultipliedAlpha)}bind(t=null,e,i){const a=this.renderer;this.current=t;let o,c,u;t?(o=t.baseTexture,u=o.resolution,e||(fr.width=t.frame.width,fr.height=t.frame.height,e=fr),i||(sl.x=t.frame.x,sl.y=t.frame.y,sl.width=e.width,sl.height=e.height,i=sl),c=o.framebuffer):(u=a.resolution,e||(fr.width=a._view.screen.width,fr.height=a._view.screen.height,e=fr),i||(i=fr,i.width=e.width,i.height=e.height));const f=this.viewportFrame;f.x=i.x*u,f.y=i.y*u,f.width=i.width*u,f.height=i.height*u,t||(f.y=a.view.height-(f.y+f.height)),f.ceil(),this.renderer.framebuffer.bind(c,f),this.renderer.projection.update(i,e,u,!c),t?this.renderer.mask.setMaskStack(o.maskStack):this.renderer.mask.setMaskStack(this.defaultMaskStack),this.sourceFrame.copyFrom(e),this.destinationFrame.copyFrom(i)}clear(t,e){const i=this.current?this.current.baseTexture.clear:this.renderer.background.backgroundColor,a=Or.shared.setValue(t||i);(this.current&&this.current.baseTexture.alphaMode>0||!this.current&&this._rendererPremultipliedAlpha)&&a.premultiply(a.alpha);const o=this.destinationFrame,c=this.current?this.current.baseTexture:this.renderer._view.screen,u=o.width!==c.width||o.height!==c.height;if(u){let{x:f,y:d,width:m,height:g}=this.viewportFrame;f=Math.round(f),d=Math.round(d),m=Math.round(m),g=Math.round(g),this.renderer.gl.enable(this.renderer.gl.SCISSOR_TEST),this.renderer.gl.scissor(f,d,m,g)}this.renderer.framebuffer.clear(a.red,a.green,a.blue,a.alpha,e),u&&this.renderer.scissor.pop()}resize(){this.bind(null)}reset(){this.bind(null)}destroy(){this.renderer=null}}Lv.extension={type:Ht.RendererSystem,name:"renderTexture"};ae.add(Lv);let xL=class{constructor(t,e){this.program=t,this.uniformData=e,this.uniformGroups={},this.uniformDirtyGroups={},this.uniformBufferBindings={}}destroy(){this.uniformData=null,this.uniformGroups=null,this.uniformDirtyGroups=null,this.uniformBufferBindings=null,this.program=null}};function TL(n,t){const e={},i=t.getProgramParameter(n,t.ACTIVE_ATTRIBUTES);for(let a=0;af>d?1:-1);for(let f=0;f({data:o,offset:0,dataLen:0,dirty:0}));let e=0,i=0,a=0;for(let o=0;o1&&(e=Math.max(e,16)*c.data.size),c.dataLen=e,i%e!==0&&i<16){const u=i%e%16;i+=u,a+=u}i+e>16?(a=Math.ceil(a/16)*16,c.offset=a,a+=e,i=e):(c.offset=a,i+=e,a+=e)}return a=Math.ceil(a/16)*16,{uboElements:t,size:a}}function wL(n,t){const e=[];for(const i in n)t[i]&&e.push(t[i]);return e.sort((i,a)=>i.index-a.index),e}function ML(n,t){if(!n.autoManage)return{size:0,syncFunc:_L};const e=wL(n.uniforms,t),{uboElements:i,size:a}=EL(e),o=[` + var v = null; + var v2 = null; + var cv = null; + var t = 0; + var gl = renderer.gl + var index = 0; + var data = buffer.data; + `];for(let c=0;c1){const g=dv(u.data.type),v=Math.max(Nv[u.data.type]/16,1),x=g/v,b=(4-x%4)%4;o.push(` + cv = ud.${d}.value; + v = uv.${d}; + offset = ${u.offset/4}; + + t = 0; + + for(var i=0; i < ${u.data.size*v}; i++) + { + for(var j = 0; j < ${x}; j++) + { + data[offset++] = v[t++]; + } + offset += ${b}; + } + + `)}else{const g=AL[u.data.type];o.push(` + cv = ud.${d}.value; + v = uv.${d}; + offset = ${u.offset/4}; + ${g}; + `)}}return o.push(` + renderer.buffer.update(buffer); + `),{size:a,syncFunc:new Function("ud","uv","renderer","syncData","buffer",o.join(` +`))}}let RL=0;const Ru={textureCount:0,uboCount:0};class Ov{constructor(t){this.destroyed=!1,this.renderer=t,this.systemCheck(),this.gl=null,this.shader=null,this.program=null,this.cache={},this._uboCache={},this.id=RL++}systemCheck(){if(!KR())throw new Error("Current environment does not allow unsafe-eval, please use @pixi/unsafe-eval module to enable support.")}contextChange(t){this.gl=t,this.reset()}bind(t,e){t.disposeRunner.add(this),t.uniforms.globals=this.renderer.globalUniforms;const i=t.program,a=i.glPrograms[this.renderer.CONTEXT_UID]||this.generateProgram(t);return this.shader=t,this.program!==i&&(this.program=i,this.gl.useProgram(a.program)),e||(Ru.textureCount=0,Ru.uboCount=0,this.syncUniformGroup(t.uniformGroup,Ru)),a}setUniforms(t){const e=this.shader.program,i=e.glPrograms[this.renderer.CONTEXT_UID];e.syncUniforms(i.uniformData,t,this.renderer)}syncUniformGroup(t,e){const i=this.getGlProgram();(!t.static||t.dirtyId!==i.uniformDirtyGroups[t.id])&&(i.uniformDirtyGroups[t.id]=t.dirtyId,this.syncUniforms(t,i,e))}syncUniforms(t,e,i){(t.syncUniforms[this.shader.program.id]||this.createSyncGroups(t))(e.uniformData,t.uniforms,this.renderer,i)}createSyncGroups(t){const e=this.getSignature(t,this.shader.program.uniformData,"u");return this.cache[e]||(this.cache[e]=XR(t,this.shader.program.uniformData)),t.syncUniforms[this.shader.program.id]=this.cache[e],t.syncUniforms[this.shader.program.id]}syncUniformBufferGroup(t,e){const i=this.getGlProgram();if(!t.static||t.dirtyId!==0||!i.uniformGroups[t.id]){t.dirtyId=0;const a=i.uniformGroups[t.id]||this.createSyncBufferGroup(t,i,e);t.buffer.update(),a(i.uniformData,t.uniforms,this.renderer,Ru,t.buffer)}this.renderer.buffer.bindBufferBase(t.buffer,i.uniformBufferBindings[e])}createSyncBufferGroup(t,e,i){const{gl:a}=this.renderer;this.renderer.buffer.bind(t.buffer);const o=this.gl.getUniformBlockIndex(e.program,i);e.uniformBufferBindings[i]=this.shader.uniformBindCount,a.uniformBlockBinding(e.program,o,this.shader.uniformBindCount),this.shader.uniformBindCount++;const c=this.getSignature(t,this.shader.program.uniformData,"ubo");let u=this._uboCache[c];if(u||(u=this._uboCache[c]=ML(t,this.shader.program.uniformData)),t.autoManage){const f=new Float32Array(u.size/4);t.buffer.update(f)}return e.uniformGroups[t.id]=u.syncFunc,e.uniformGroups[t.id]}getSignature(t,e,i){const a=t.uniforms,o=[`${i}-`];for(const c in a)o.push(c),e[c]&&o.push(e[c].type);return o.join("-")}getGlProgram(){return this.shader?this.shader.program.glPrograms[this.renderer.CONTEXT_UID]:null}generateProgram(t){const e=this.gl,i=t.program,a=SL(e,i);return i.glPrograms[this.renderer.CONTEXT_UID]=a,a}reset(){this.program=null,this.shader=null}disposeShader(t){this.shader===t&&(this.shader=null)}destroy(){this.renderer=null,this.destroyed=!0}}Ov.extension={type:Ht.RendererSystem,name:"shader"};ae.add(Ov);class dh{constructor(t){this.renderer=t}run(t){const{renderer:e}=this;e.runners.init.emit(e.options),t.hello&&console.log(`PixiJS 7.4.3 - ${e.rendererLogId} - https://pixijs.com`),e.resize(e.screen.width,e.screen.height)}destroy(){}}dh.defaultOptions={hello:!1},dh.extension={type:[Ht.RendererSystem,Ht.CanvasRendererSystem],name:"startup"};ae.add(dh);function LL(n,t=[]){return t[Nt.NORMAL]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.ADD]=[n.ONE,n.ONE],t[Nt.MULTIPLY]=[n.DST_COLOR,n.ONE_MINUS_SRC_ALPHA,n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.SCREEN]=[n.ONE,n.ONE_MINUS_SRC_COLOR,n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.OVERLAY]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.DARKEN]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.LIGHTEN]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.COLOR_DODGE]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.COLOR_BURN]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.HARD_LIGHT]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.SOFT_LIGHT]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.DIFFERENCE]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.EXCLUSION]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.HUE]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.SATURATION]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.COLOR]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.LUMINOSITY]=[n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.NONE]=[0,0],t[Nt.NORMAL_NPM]=[n.SRC_ALPHA,n.ONE_MINUS_SRC_ALPHA,n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.ADD_NPM]=[n.SRC_ALPHA,n.ONE,n.ONE,n.ONE],t[Nt.SCREEN_NPM]=[n.SRC_ALPHA,n.ONE_MINUS_SRC_COLOR,n.ONE,n.ONE_MINUS_SRC_ALPHA],t[Nt.SRC_IN]=[n.DST_ALPHA,n.ZERO],t[Nt.SRC_OUT]=[n.ONE_MINUS_DST_ALPHA,n.ZERO],t[Nt.SRC_ATOP]=[n.DST_ALPHA,n.ONE_MINUS_SRC_ALPHA],t[Nt.DST_OVER]=[n.ONE_MINUS_DST_ALPHA,n.ONE],t[Nt.DST_IN]=[n.ZERO,n.SRC_ALPHA],t[Nt.DST_OUT]=[n.ZERO,n.ONE_MINUS_SRC_ALPHA],t[Nt.DST_ATOP]=[n.ONE_MINUS_DST_ALPHA,n.SRC_ALPHA],t[Nt.XOR]=[n.ONE_MINUS_DST_ALPHA,n.ONE_MINUS_SRC_ALPHA],t[Nt.SUBTRACT]=[n.ONE,n.ONE,n.ONE,n.ONE,n.FUNC_REVERSE_SUBTRACT,n.FUNC_ADD],t}const NL=0,OL=1,IL=2,DL=3,BL=4,PL=5,Iv=class Em{constructor(){this.gl=null,this.stateId=0,this.polygonOffset=0,this.blendMode=Nt.NONE,this._blendEq=!1,this.map=[],this.map[NL]=this.setBlend,this.map[OL]=this.setOffset,this.map[IL]=this.setCullFace,this.map[DL]=this.setDepthTest,this.map[BL]=this.setFrontFace,this.map[PL]=this.setDepthMask,this.checks=[],this.defaultState=new eo,this.defaultState.blend=!0}contextChange(t){this.gl=t,this.blendModes=LL(t),this.set(this.defaultState),this.reset()}set(t){if(t=t||this.defaultState,this.stateId!==t.data){let e=this.stateId^t.data,i=0;for(;e;)e&1&&this.map[i].call(this,!!(t.data&1<>1,i++;this.stateId=t.data}for(let e=0;et.systems[a]),i=[...e,...Object.keys(t.systems).filter(a=>!e.includes(a))];for(const a of i)this.addSystem(t.systems[a],a)}addRunners(...t){t.forEach(e=>{this.runners[e]=new ui(e)})}addSystem(t,e){const i=new t(this);if(this[e])throw new Error(`Whoops! The name "${e}" is already in use`);this[e]=i,this._systemsHash[e]=i;for(const a in this.runners)this.runners[a].add(i);return this}emitWithCustomOptions(t,e){const i=Object.keys(this._systemsHash);t.items.forEach(a=>{const o=i.find(c=>this._systemsHash[c]===a);a[t.name](e[o])})}destroy(){Object.values(this.runners).forEach(t=>{t.destroy()}),this._systemsHash={}}}const gl=class Yu{constructor(t){this.renderer=t,this.count=0,this.checkCount=0,this.maxIdle=Yu.defaultMaxIdle,this.checkCountMax=Yu.defaultCheckCountMax,this.mode=Yu.defaultMode}postrender(){this.renderer.objectRenderer.renderingToScreen&&(this.count++,this.mode!==_2.MANUAL&&(this.checkCount++,this.checkCount>this.checkCountMax&&(this.checkCount=0,this.run())))}run(){const t=this.renderer.texture,e=t.managedTextures;let i=!1;for(let a=0;athis.maxIdle&&(t.destroyTexture(o,!0),e[a]=null,i=!0)}if(i){let a=0;for(let o=0;o=0;a--)this.unload(t.children[a])}destroy(){this.renderer=null}};gl.defaultMode=_2.AUTO,gl.defaultMaxIdle=3600,gl.defaultCheckCountMax=600,gl.extension={type:Ht.RendererSystem,name:"textureGC"};let Tr=gl;ae.add(Tr);let R0=class{constructor(t){this.texture=t,this.width=-1,this.height=-1,this.dirtyId=-1,this.dirtyStyleId=-1,this.mipmap=!1,this.wrapMode=33071,this.type=jt.UNSIGNED_BYTE,this.internalFormat=gt.RGBA,this.samplerType=0}};function kL(n){let t;return"WebGL2RenderingContext"in globalThis&&n instanceof globalThis.WebGL2RenderingContext?t={[n.RGB]:vt.FLOAT,[n.RGBA]:vt.FLOAT,[n.ALPHA]:vt.FLOAT,[n.LUMINANCE]:vt.FLOAT,[n.LUMINANCE_ALPHA]:vt.FLOAT,[n.R8]:vt.FLOAT,[n.R8_SNORM]:vt.FLOAT,[n.RG8]:vt.FLOAT,[n.RG8_SNORM]:vt.FLOAT,[n.RGB8]:vt.FLOAT,[n.RGB8_SNORM]:vt.FLOAT,[n.RGB565]:vt.FLOAT,[n.RGBA4]:vt.FLOAT,[n.RGB5_A1]:vt.FLOAT,[n.RGBA8]:vt.FLOAT,[n.RGBA8_SNORM]:vt.FLOAT,[n.RGB10_A2]:vt.FLOAT,[n.RGB10_A2UI]:vt.FLOAT,[n.SRGB8]:vt.FLOAT,[n.SRGB8_ALPHA8]:vt.FLOAT,[n.R16F]:vt.FLOAT,[n.RG16F]:vt.FLOAT,[n.RGB16F]:vt.FLOAT,[n.RGBA16F]:vt.FLOAT,[n.R32F]:vt.FLOAT,[n.RG32F]:vt.FLOAT,[n.RGB32F]:vt.FLOAT,[n.RGBA32F]:vt.FLOAT,[n.R11F_G11F_B10F]:vt.FLOAT,[n.RGB9_E5]:vt.FLOAT,[n.R8I]:vt.INT,[n.R8UI]:vt.UINT,[n.R16I]:vt.INT,[n.R16UI]:vt.UINT,[n.R32I]:vt.INT,[n.R32UI]:vt.UINT,[n.RG8I]:vt.INT,[n.RG8UI]:vt.UINT,[n.RG16I]:vt.INT,[n.RG16UI]:vt.UINT,[n.RG32I]:vt.INT,[n.RG32UI]:vt.UINT,[n.RGB8I]:vt.INT,[n.RGB8UI]:vt.UINT,[n.RGB16I]:vt.INT,[n.RGB16UI]:vt.UINT,[n.RGB32I]:vt.INT,[n.RGB32UI]:vt.UINT,[n.RGBA8I]:vt.INT,[n.RGBA8UI]:vt.UINT,[n.RGBA16I]:vt.INT,[n.RGBA16UI]:vt.UINT,[n.RGBA32I]:vt.INT,[n.RGBA32UI]:vt.UINT,[n.DEPTH_COMPONENT16]:vt.FLOAT,[n.DEPTH_COMPONENT24]:vt.FLOAT,[n.DEPTH_COMPONENT32F]:vt.FLOAT,[n.DEPTH_STENCIL]:vt.FLOAT,[n.DEPTH24_STENCIL8]:vt.FLOAT,[n.DEPTH32F_STENCIL8]:vt.FLOAT}:t={[n.RGB]:vt.FLOAT,[n.RGBA]:vt.FLOAT,[n.ALPHA]:vt.FLOAT,[n.LUMINANCE]:vt.FLOAT,[n.LUMINANCE_ALPHA]:vt.FLOAT,[n.DEPTH_STENCIL]:vt.FLOAT},t}function zL(n){let t;return"WebGL2RenderingContext"in globalThis&&n instanceof globalThis.WebGL2RenderingContext?t={[jt.UNSIGNED_BYTE]:{[gt.RGBA]:n.RGBA8,[gt.RGB]:n.RGB8,[gt.RG]:n.RG8,[gt.RED]:n.R8,[gt.RGBA_INTEGER]:n.RGBA8UI,[gt.RGB_INTEGER]:n.RGB8UI,[gt.RG_INTEGER]:n.RG8UI,[gt.RED_INTEGER]:n.R8UI,[gt.ALPHA]:n.ALPHA,[gt.LUMINANCE]:n.LUMINANCE,[gt.LUMINANCE_ALPHA]:n.LUMINANCE_ALPHA},[jt.BYTE]:{[gt.RGBA]:n.RGBA8_SNORM,[gt.RGB]:n.RGB8_SNORM,[gt.RG]:n.RG8_SNORM,[gt.RED]:n.R8_SNORM,[gt.RGBA_INTEGER]:n.RGBA8I,[gt.RGB_INTEGER]:n.RGB8I,[gt.RG_INTEGER]:n.RG8I,[gt.RED_INTEGER]:n.R8I},[jt.UNSIGNED_SHORT]:{[gt.RGBA_INTEGER]:n.RGBA16UI,[gt.RGB_INTEGER]:n.RGB16UI,[gt.RG_INTEGER]:n.RG16UI,[gt.RED_INTEGER]:n.R16UI,[gt.DEPTH_COMPONENT]:n.DEPTH_COMPONENT16},[jt.SHORT]:{[gt.RGBA_INTEGER]:n.RGBA16I,[gt.RGB_INTEGER]:n.RGB16I,[gt.RG_INTEGER]:n.RG16I,[gt.RED_INTEGER]:n.R16I},[jt.UNSIGNED_INT]:{[gt.RGBA_INTEGER]:n.RGBA32UI,[gt.RGB_INTEGER]:n.RGB32UI,[gt.RG_INTEGER]:n.RG32UI,[gt.RED_INTEGER]:n.R32UI,[gt.DEPTH_COMPONENT]:n.DEPTH_COMPONENT24},[jt.INT]:{[gt.RGBA_INTEGER]:n.RGBA32I,[gt.RGB_INTEGER]:n.RGB32I,[gt.RG_INTEGER]:n.RG32I,[gt.RED_INTEGER]:n.R32I},[jt.FLOAT]:{[gt.RGBA]:n.RGBA32F,[gt.RGB]:n.RGB32F,[gt.RG]:n.RG32F,[gt.RED]:n.R32F,[gt.DEPTH_COMPONENT]:n.DEPTH_COMPONENT32F},[jt.HALF_FLOAT]:{[gt.RGBA]:n.RGBA16F,[gt.RGB]:n.RGB16F,[gt.RG]:n.RG16F,[gt.RED]:n.R16F},[jt.UNSIGNED_SHORT_5_6_5]:{[gt.RGB]:n.RGB565},[jt.UNSIGNED_SHORT_4_4_4_4]:{[gt.RGBA]:n.RGBA4},[jt.UNSIGNED_SHORT_5_5_5_1]:{[gt.RGBA]:n.RGB5_A1},[jt.UNSIGNED_INT_2_10_10_10_REV]:{[gt.RGBA]:n.RGB10_A2,[gt.RGBA_INTEGER]:n.RGB10_A2UI},[jt.UNSIGNED_INT_10F_11F_11F_REV]:{[gt.RGB]:n.R11F_G11F_B10F},[jt.UNSIGNED_INT_5_9_9_9_REV]:{[gt.RGB]:n.RGB9_E5},[jt.UNSIGNED_INT_24_8]:{[gt.DEPTH_STENCIL]:n.DEPTH24_STENCIL8},[jt.FLOAT_32_UNSIGNED_INT_24_8_REV]:{[gt.DEPTH_STENCIL]:n.DEPTH32F_STENCIL8}}:t={[jt.UNSIGNED_BYTE]:{[gt.RGBA]:n.RGBA,[gt.RGB]:n.RGB,[gt.ALPHA]:n.ALPHA,[gt.LUMINANCE]:n.LUMINANCE,[gt.LUMINANCE_ALPHA]:n.LUMINANCE_ALPHA},[jt.UNSIGNED_SHORT_5_6_5]:{[gt.RGB]:n.RGB},[jt.UNSIGNED_SHORT_4_4_4_4]:{[gt.RGBA]:n.RGBA},[jt.UNSIGNED_SHORT_5_5_5_1]:{[gt.RGBA]:n.RGBA}},t}class Dv{constructor(t){this.renderer=t,this.boundTextures=[],this.currentLocation=-1,this.managedTextures=[],this._unknownBoundTextures=!1,this.unknownTexture=new ge,this.hasIntegerTextures=!1}contextChange(){const t=this.gl=this.renderer.gl;this.CONTEXT_UID=this.renderer.CONTEXT_UID,this.webGLVersion=this.renderer.context.webGLVersion,this.internalFormats=zL(t),this.samplerTypes=kL(t);const e=t.getParameter(t.MAX_TEXTURE_IMAGE_UNITS);this.boundTextures.length=e;for(let a=0;a=0;--o){const c=e[o];c&&c._glTextures[a].samplerType!==vt.FLOAT&&this.renderer.texture.unbind(c)}}initTexture(t){const e=new R0(this.gl.createTexture());return e.dirtyId=-1,t._glTextures[this.CONTEXT_UID]=e,this.managedTextures.push(t),t.on("dispose",this.destroyTexture,this),e}initTextureType(t,e){e.internalFormat=this.internalFormats[t.type]?.[t.format]??t.format,e.samplerType=this.samplerTypes[e.internalFormat]??vt.FLOAT,this.webGLVersion===2&&t.type===jt.HALF_FLOAT?e.type=this.gl.HALF_FLOAT:e.type=t.type}updateTexture(t){const e=t._glTextures[this.CONTEXT_UID];if(!e)return;const i=this.renderer;if(this.initTextureType(t,e),t.resource?.upload(i,t,e))e.samplerType!==vt.FLOAT&&(this.hasIntegerTextures=!0);else{const a=t.realWidth,o=t.realHeight,c=i.gl;(e.width!==a||e.height!==o||e.dirtyId<0)&&(e.width=a,e.height=o,c.texImage2D(t.target,0,e.internalFormat,a,o,0,t.format,e.type,null))}t.dirtyStyleId!==e.dirtyStyleId&&this.updateTextureStyle(t),e.dirtyId=t.dirtyId}destroyTexture(t,e){const{gl:i}=this;if(t=t.castToBaseTexture(),t._glTextures[this.CONTEXT_UID]&&(this.unbind(t),i.deleteTexture(t._glTextures[this.CONTEXT_UID].texture),t.off("dispose",this.destroyTexture,this),delete t._glTextures[this.CONTEXT_UID],!e)){const a=this.managedTextures.indexOf(t);a!==-1&&qu(this.managedTextures,a,1)}}updateTextureStyle(t){const e=t._glTextures[this.CONTEXT_UID];e&&((t.mipmap===Nr.POW2||this.webGLVersion!==2)&&!t.isPowerOfTwo?e.mipmap=!1:e.mipmap=t.mipmap>=1,this.webGLVersion!==2&&!t.isPowerOfTwo?e.wrapMode=S2.CLAMP:e.wrapMode=t.wrapMode,t.resource?.style(this.renderer,t,e)||this.setStyle(t,e),e.dirtyStyleId=t.dirtyStyleId)}setStyle(t,e){const i=this.gl;if(e.mipmap&&t.mipmap!==Nr.ON_MANUAL&&i.generateMipmap(t.target),i.texParameteri(t.target,i.TEXTURE_WRAP_S,e.wrapMode),i.texParameteri(t.target,i.TEXTURE_WRAP_T,e.wrapMode),e.mipmap){i.texParameteri(t.target,i.TEXTURE_MIN_FILTER,t.scaleMode===es.LINEAR?i.LINEAR_MIPMAP_LINEAR:i.NEAREST_MIPMAP_NEAREST);const a=this.renderer.context.extensions.anisotropicFiltering;if(a&&t.anisotropicLevel>0&&t.scaleMode===es.LINEAR){const o=Math.min(t.anisotropicLevel,i.getParameter(a.MAX_TEXTURE_MAX_ANISOTROPY_EXT));i.texParameterf(t.target,a.TEXTURE_MAX_ANISOTROPY_EXT,o)}}else i.texParameteri(t.target,i.TEXTURE_MIN_FILTER,t.scaleMode===es.LINEAR?i.LINEAR:i.NEAREST);i.texParameteri(t.target,i.TEXTURE_MAG_FILTER,t.scaleMode===es.LINEAR?i.LINEAR:i.NEAREST)}destroy(){this.renderer=null}}Dv.extension={type:Ht.RendererSystem,name:"texture"};ae.add(Dv);class Bv{constructor(t){this.renderer=t}contextChange(){this.gl=this.renderer.gl,this.CONTEXT_UID=this.renderer.CONTEXT_UID}bind(t){const{gl:e,CONTEXT_UID:i}=this,a=t._glTransformFeedbacks[i]||this.createGLTransformFeedback(t);e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,a)}unbind(){const{gl:t}=this;t.bindTransformFeedback(t.TRANSFORM_FEEDBACK,null)}beginTransformFeedback(t,e){const{gl:i,renderer:a}=this;e&&a.shader.bind(e),i.beginTransformFeedback(t)}endTransformFeedback(){const{gl:t}=this;t.endTransformFeedback()}createGLTransformFeedback(t){const{gl:e,renderer:i,CONTEXT_UID:a}=this,o=e.createTransformFeedback();t._glTransformFeedbacks[a]=o,e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,o);for(let c=0;c(n[n.INTERACTION=50]="INTERACTION",n[n.HIGH=25]="HIGH",n[n.NORMAL=0]="NORMAL",n[n.LOW=-25]="LOW",n[n.UTILITY=-50]="UTILITY",n))(ph||{});class L0{constructor(t,e=null,i=0,a=!1){this.next=null,this.previous=null,this._destroyed=!1,this.fn=t,this.context=e,this.priority=i,this.once=a}match(t,e=null){return this.fn===t&&this.context===e}emit(t){this.fn&&(this.context?this.fn.call(this.context,t):this.fn(t));const e=this.next;return this.once&&this.destroy(!0),this._destroyed&&(this.next=null),e}connect(t){this.previous=t,t.next&&(t.next.previous=this),this.next=t.next,t.next=this}destroy(t=!1){this._destroyed=!0,this.fn=null,this.context=null,this.previous&&(this.previous.next=this.next),this.next&&(this.next.previous=this.previous);const e=this.next;return this.next=t?null:e,this.previous=null,e}}const Pv=class In{constructor(){this.autoStart=!1,this.deltaTime=1,this.lastTime=-1,this.speed=1,this.started=!1,this._requestId=null,this._maxElapsedMS=100,this._minElapsedMS=0,this._protected=!1,this._lastFrame=-1,this._head=new L0(null,null,1/0),this.deltaMS=1/In.targetFPMS,this.elapsedMS=1/In.targetFPMS,this._tick=t=>{this._requestId=null,this.started&&(this.update(t),this.started&&this._requestId===null&&this._head.next&&(this._requestId=requestAnimationFrame(this._tick)))}}_requestIfNeeded(){this._requestId===null&&this._head.next&&(this.lastTime=performance.now(),this._lastFrame=this.lastTime,this._requestId=requestAnimationFrame(this._tick))}_cancelIfNeeded(){this._requestId!==null&&(cancelAnimationFrame(this._requestId),this._requestId=null)}_startIfPossible(){this.started?this._requestIfNeeded():this.autoStart&&this.start()}add(t,e,i=ph.NORMAL){return this._addListener(new L0(t,e,i))}addOnce(t,e,i=ph.NORMAL){return this._addListener(new L0(t,e,i,!0))}_addListener(t){let e=this._head.next,i=this._head;if(!e)t.connect(i);else{for(;e;){if(t.priority>e.priority){t.connect(i);break}i=e,e=e.next}t.previous||t.connect(i)}return this._startIfPossible(),this}remove(t,e){let i=this._head.next;for(;i;)i.match(t,e)?i=i.destroy():i=i.next;return this._head.next||this._cancelIfNeeded(),this}get count(){if(!this._head)return 0;let t=0,e=this._head;for(;e=e.next;)t++;return t}start(){this.started||(this.started=!0,this._requestIfNeeded())}stop(){this.started&&(this.started=!1,this._cancelIfNeeded())}destroy(){if(!this._protected){this.stop();let t=this._head.next;for(;t;)t=t.destroy(!0);this._head.destroy(),this._head=null}}update(t=performance.now()){let e;if(t>this.lastTime){if(e=this.elapsedMS=t-this.lastTime,e>this._maxElapsedMS&&(e=this._maxElapsedMS),e*=this.speed,this._minElapsedMS){const o=t-this._lastFrame|0;if(o{this._ticker.stop()},this.start=()=>{this._ticker.start()},this._ticker=null,this.ticker=t.sharedTicker?Us.shared:new Us,t.autoStart&&this.start()}static destroy(){if(this._ticker){const t=this._ticker;this.ticker=null,t.destroy()}}}Fv.extension=Ht.Application;ae.add(Fv);const Uv=[];ae.handleByList(Ht.Renderer,Uv);function VL(n){for(const t of Uv)if(t.test(n))return new t(n);throw new Error("Unable to auto-detect a suitable renderer.")}var GL=`attribute vec2 aVertexPosition; + +uniform mat3 projectionMatrix; + +varying vec2 vTextureCoord; + +uniform vec4 inputSize; +uniform vec4 outputFrame; + +vec4 filterVertexPosition( void ) +{ + vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; + + return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); +} + +vec2 filterTextureCoord( void ) +{ + return aVertexPosition * (outputFrame.zw * inputSize.zw); +} + +void main(void) +{ + gl_Position = filterVertexPosition(); + vTextureCoord = filterTextureCoord(); +} +`;const jL=GL;class kv{constructor(t){this.renderer=t}contextChange(t){let e;if(this.renderer.context.webGLVersion===1){const i=t.getParameter(t.FRAMEBUFFER_BINDING);t.bindFramebuffer(t.FRAMEBUFFER,null),e=t.getParameter(t.SAMPLES),t.bindFramebuffer(t.FRAMEBUFFER,i)}else{const i=t.getParameter(t.DRAW_FRAMEBUFFER_BINDING);t.bindFramebuffer(t.DRAW_FRAMEBUFFER,null),e=t.getParameter(t.SAMPLES),t.bindFramebuffer(t.DRAW_FRAMEBUFFER,i)}e>=Ge.HIGH?this.multisample=Ge.HIGH:e>=Ge.MEDIUM?this.multisample=Ge.MEDIUM:e>=Ge.LOW?this.multisample=Ge.LOW:this.multisample=Ge.NONE}destroy(){}}kv.extension={type:Ht.RendererSystem,name:"_multisample"};ae.add(kv);class HL{constructor(t){this.buffer=t||null,this.updateID=-1,this.byteLength=-1,this.refCount=0}}class zv{constructor(t){this.renderer=t,this.managedBuffers={},this.boundBufferBases={}}destroy(){this.renderer=null}contextChange(){this.disposeAll(!0),this.gl=this.renderer.gl,this.CONTEXT_UID=this.renderer.CONTEXT_UID}bind(t){const{gl:e,CONTEXT_UID:i}=this,a=t._glBuffers[i]||this.createGLBuffer(t);e.bindBuffer(t.type,a.buffer)}unbind(t){const{gl:e}=this;e.bindBuffer(t,null)}bindBufferBase(t,e){const{gl:i,CONTEXT_UID:a}=this;if(this.boundBufferBases[e]!==t){const o=t._glBuffers[a]||this.createGLBuffer(t);this.boundBufferBases[e]=t,i.bindBufferBase(i.UNIFORM_BUFFER,e,o.buffer)}}bindBufferRange(t,e,i){const{gl:a,CONTEXT_UID:o}=this;i=i||0;const c=t._glBuffers[o]||this.createGLBuffer(t);a.bindBufferRange(a.UNIFORM_BUFFER,e||0,c.buffer,i*256,256)}update(t){const{gl:e,CONTEXT_UID:i}=this,a=t._glBuffers[i]||this.createGLBuffer(t);if(t._updateID!==a.updateID)if(a.updateID=t._updateID,e.bindBuffer(t.type,a.buffer),a.byteLength>=t.data.byteLength)e.bufferSubData(t.type,0,t.data);else{const o=t.static?e.STATIC_DRAW:e.DYNAMIC_DRAW;a.byteLength=t.data.byteLength,e.bufferData(t.type,t.data,o)}}dispose(t,e){if(!this.managedBuffers[t.id])return;delete this.managedBuffers[t.id];const i=t._glBuffers[this.CONTEXT_UID],a=this.gl;t.disposeRunner.remove(this),i&&(e||a.deleteBuffer(i.buffer),delete t._glBuffers[this.CONTEXT_UID])}disposeAll(t){const e=Object.keys(this.managedBuffers);for(let i=0;ie.resource).filter(e=>e).map(e=>e.load());return this._load=Promise.all(t).then(()=>{const{realWidth:e,realHeight:i}=this.items[0];return this.resize(e,i),this.update(),Promise.resolve(this)}),this._load}}class qL extends Gv{constructor(t,e){const{width:i,height:a}=e||{};let o,c;Array.isArray(t)?(o=t,c=t.length):c=t,super(c,{width:i,height:a}),o&&this.initFromArray(o,e)}addBaseTextureAt(t,e){if(t.resource)this.addResourceAt(t.resource,e);else throw new Error("ArrayResource does not support RenderTexture");return this}bind(t){super.bind(t),t.target=ja.TEXTURE_2D_ARRAY}upload(t,e,i){const{length:a,itemDirtyIds:o,items:c}=this,{gl:u}=t;i.dirtyId<0&&u.texImage3D(u.TEXTURE_2D_ARRAY,0,i.internalFormat,this._width,this._height,a,0,e.format,i.type,null);for(let f=0;f0)if(t.resource)this.addResourceAt(t.resource,e);else throw new Error("CubeResource does not support copying of renderTexture.");else t.target=ja.TEXTURE_CUBE_MAP_POSITIVE_X+e,t.parentTextureArray=this.baseTexture,this.items[e]=t;return t.valid&&!this.valid&&this.resize(t.realWidth,t.realHeight),this.items[e]=t,this}upload(t,e,i){const a=this.itemDirtyIds;for(let o=0;o{if(this.url===null){t(this);return}try{const i=await Kt.ADAPTER.fetch(this.url,{mode:this.crossOrigin?"cors":"no-cors"});if(this.destroyed)return;const a=await i.blob();if(this.destroyed)return;const o=await createImageBitmap(a,{premultiplyAlpha:this.alphaMode===null||this.alphaMode===Pr.UNPACK?"premultiply":"none"});if(this.destroyed){o.close();return}this.source=o,this.update(),t(this)}catch(i){if(this.destroyed)return;e(i),this.onError.emit(i)}}),this._load)}upload(t,e,i){return this.source instanceof ImageBitmap?(typeof this.alphaMode=="number"&&(e.alphaMode=this.alphaMode),super.upload(t,e,i)):(this.load(),!1)}dispose(){this.ownsImageBitmap&&this.source instanceof ImageBitmap&&this.source.close(),super.dispose(),this._load=null}static test(t){return!!globalThis.createImageBitmap&&typeof ImageBitmap<"u"&&(typeof t=="string"||t instanceof ImageBitmap)}static get EMPTY(){return za._EMPTY=za._EMPTY??Kt.ADAPTER.createCanvas(0,0),za._EMPTY}}const Mm=class Wu extends is{constructor(t,e){e=e||{},super(Kt.ADAPTER.createCanvas()),this._width=0,this._height=0,this.svg=t,this.scale=e.scale||1,this._overrideWidth=e.width,this._overrideHeight=e.height,this._resolve=null,this._crossorigin=e.crossorigin,this._load=null,e.autoLoad!==!1&&this.load()}load(){return this._load?this._load:(this._load=new Promise(t=>{if(this._resolve=()=>{this.update(),t(this)},Wu.SVG_XML.test(this.svg.trim())){if(!btoa)throw new Error("Your browser doesn't support base64 conversions.");this.svg=`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(this.svg)))}`}this._loadSvg()}),this._load)}_loadSvg(){const t=new Image;is.crossOrigin(t,this.svg,this._crossorigin),t.src=this.svg,t.onerror=e=>{this._resolve&&(t.onerror=null,this.onError.emit(e))},t.onload=()=>{if(!this._resolve)return;const e=t.width,i=t.height;if(!e||!i)throw new Error("The SVG image must have width and height defined (in pixels), canvas API needs them.");let a=e*this.scale,o=i*this.scale;(this._overrideWidth||this._overrideHeight)&&(a=this._overrideWidth||this._overrideHeight/i*e,o=this._overrideHeight||this._overrideWidth/e*i),a=Math.round(a),o=Math.round(o);const c=this.source;c.width=a,c.height=o,c._pixiId=`canvas_${Ul()}`,c.getContext("2d").drawImage(t,0,0,e,i,0,0,a,o),this._resolve(),this._resolve=null}}static getSize(t){const e=Wu.SVG_SIZE.exec(t),i={};return e&&(i[e[1]]=Math.round(parseFloat(e[3])),i[e[5]]=Math.round(parseFloat(e[7]))),i}dispose(){super.dispose(),this._resolve=null,this._crossorigin=null}static test(t,e){return e==="svg"||typeof t=="string"&&t.startsWith("data:image/svg+xml")||typeof t=="string"&&Wu.SVG_XML.test(t)}};Mm.SVG_XML=/^(<\?xml[^?]+\?>)?\s*()]*-->)?\s*\]*(?:\s(width|height)=('|")(\d*(?:\.\d+)?)(?:px)?('|"))[^>]*(?:\s(width|height)=('|")(\d*(?:\.\d+)?)(?:px)?('|"))[^>]*>/i;let $L=Mm;class WL extends is{constructor(t){super(t)}static test(t){return!!globalThis.VideoFrame&&t instanceof globalThis.VideoFrame}}const Rm=class Lm extends is{constructor(t,e){if(e=e||{},!(t instanceof HTMLVideoElement)){const i=document.createElement("video");e.autoLoad!==!1&&i.setAttribute("preload","auto"),e.playsinline!==!1&&(i.setAttribute("webkit-playsinline",""),i.setAttribute("playsinline","")),e.muted===!0&&(i.setAttribute("muted",""),i.muted=!0),e.loop===!0&&i.setAttribute("loop",""),e.autoPlay!==!1&&i.setAttribute("autoplay",""),typeof t=="string"&&(t=[t]);const a=t[0].src||t[0];is.crossOrigin(i,a,e.crossorigin);for(let o=0;o{this.valid?e(this):(this._resolve=e,this._reject=i,t.load())}),this._load}_onError(t){this.source.removeEventListener("error",this._onError,!0),this.onError.emit(t),this._reject&&(this._reject(t),this._reject=null,this._resolve=null)}_isSourcePlaying(){const t=this.source;return!t.paused&&!t.ended}_isSourceReady(){return this.source.readyState>2}_onPlayStart(){this.valid||this._onCanPlay(),this._configureAutoUpdate()}_onPlayStop(){this._configureAutoUpdate()}_onSeeked(){this._autoUpdate&&!this._isSourcePlaying()&&(this._msToNextUpdate=0,this.update(),this._msToNextUpdate=0)}_onCanPlay(){const t=this.source;t.removeEventListener("canplay",this._onCanPlay),t.removeEventListener("canplaythrough",this._onCanPlay);const e=this.valid;this._msToNextUpdate=0,this.update(),this._msToNextUpdate=0,!e&&this._resolve&&(this._resolve(this),this._resolve=null,this._reject=null),this._isSourcePlaying()?this._onPlayStart():this.autoPlay&&t.play()}dispose(){this._configureAutoUpdate();const t=this.source;t&&(t.removeEventListener("play",this._onPlayStart),t.removeEventListener("pause",this._onPlayStop),t.removeEventListener("seeked",this._onSeeked),t.removeEventListener("canplay",this._onCanPlay),t.removeEventListener("canplaythrough",this._onCanPlay),t.removeEventListener("error",this._onError,!0),t.pause(),t.src="",t.load()),super.dispose()}get autoUpdate(){return this._autoUpdate}set autoUpdate(t){t!==this._autoUpdate&&(this._autoUpdate=t,this._configureAutoUpdate())}get updateFPS(){return this._updateFPS}set updateFPS(t){t!==this._updateFPS&&(this._updateFPS=t,this._configureAutoUpdate())}_configureAutoUpdate(){this._autoUpdate&&this._isSourcePlaying()?!this._updateFPS&&this.source.requestVideoFrameCallback?(this._isConnectedToTicker&&(Us.shared.remove(this.update,this),this._isConnectedToTicker=!1,this._msToNextUpdate=0),this._videoFrameRequestCallbackHandle===null&&(this._videoFrameRequestCallbackHandle=this.source.requestVideoFrameCallback(this._videoFrameRequestCallback))):(this._videoFrameRequestCallbackHandle!==null&&(this.source.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle),this._videoFrameRequestCallbackHandle=null),this._isConnectedToTicker||(Us.shared.add(this.update,this),this._isConnectedToTicker=!0,this._msToNextUpdate=0)):(this._videoFrameRequestCallbackHandle!==null&&(this.source.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle),this._videoFrameRequestCallbackHandle=null),this._isConnectedToTicker&&(Us.shared.remove(this.update,this),this._isConnectedToTicker=!1,this._msToNextUpdate=0))}static test(t,e){return globalThis.HTMLVideoElement&&t instanceof HTMLVideoElement||Lm.TYPES.includes(e)}};Rm.TYPES=["mp4","m4v","webm","ogg","ogv","h264","avi","mov"],Rm.MIME_TYPES={ogv:"video/ogg",mov:"video/quicktime",m4v:"video/mp4"};let ZL=Rm;bm.push(za,vv,XL,ZL,WL,$L,lv,YL,qL);class Nm{constructor(){this.minX=1/0,this.minY=1/0,this.maxX=-1/0,this.maxY=-1/0,this.rect=null,this.updateID=-1}isEmpty(){return this.minX>this.maxX||this.minY>this.maxY}clear(){this.minX=1/0,this.minY=1/0,this.maxX=-1/0,this.maxY=-1/0}getRectangle(t){return this.minX>this.maxX||this.minY>this.maxY?be.EMPTY:(t=t||new be(0,0,1,1),t.x=this.minX,t.y=this.minY,t.width=this.maxX-this.minX,t.height=this.maxY-this.minY,t)}addPoint(t){this.minX=Math.min(this.minX,t.x),this.maxX=Math.max(this.maxX,t.x),this.minY=Math.min(this.minY,t.y),this.maxY=Math.max(this.maxY,t.y)}addPointMatrix(t,e){const{a:i,b:a,c:o,d:c,tx:u,ty:f}=t,d=i*e.x+o*e.y+u,m=a*e.x+c*e.y+f;this.minX=Math.min(this.minX,d),this.maxX=Math.max(this.maxX,d),this.minY=Math.min(this.minY,m),this.maxY=Math.max(this.maxY,m)}addQuad(t){let e=this.minX,i=this.minY,a=this.maxX,o=this.maxY,c=t[0],u=t[1];e=ca?c:a,o=u>o?u:o,c=t[2],u=t[3],e=ca?c:a,o=u>o?u:o,c=t[4],u=t[5],e=ca?c:a,o=u>o?u:o,c=t[6],u=t[7],e=ca?c:a,o=u>o?u:o,this.minX=e,this.minY=i,this.maxX=a,this.maxY=o}addFrame(t,e,i,a,o){this.addFrameMatrix(t.worldTransform,e,i,a,o)}addFrameMatrix(t,e,i,a,o){const c=t.a,u=t.b,f=t.c,d=t.d,m=t.tx,g=t.ty;let v=this.minX,x=this.minY,b=this.maxX,C=this.maxY,T=c*e+f*i+m,S=u*e+d*i+g;v=Tb?T:b,C=S>C?S:C,T=c*a+f*i+m,S=u*a+d*i+g,v=Tb?T:b,C=S>C?S:C,T=c*e+f*o+m,S=u*e+d*o+g,v=Tb?T:b,C=S>C?S:C,T=c*a+f*o+m,S=u*a+d*o+g,v=Tb?T:b,C=S>C?S:C,this.minX=v,this.minY=x,this.maxX=b,this.maxY=C}addVertexData(t,e,i){let a=this.minX,o=this.minY,c=this.maxX,u=this.maxY;for(let f=e;fc?d:c,u=m>u?m:u}this.minX=a,this.minY=o,this.maxX=c,this.maxY=u}addVertices(t,e,i,a){this.addVerticesMatrix(t.worldTransform,e,i,a)}addVerticesMatrix(t,e,i,a,o=0,c=o){const u=t.a,f=t.b,d=t.c,m=t.d,g=t.tx,v=t.ty;let x=this.minX,b=this.minY,C=this.maxX,T=this.maxY;for(let S=i;Sa?t.maxX:a,this.maxY=t.maxY>o?t.maxY:o}addBoundsMask(t,e){const i=t.minX>e.minX?t.minX:e.minX,a=t.minY>e.minY?t.minY:e.minY,o=t.maxXd?o:d,this.maxY=c>m?c:m}}addBoundsMatrix(t,e){this.addFrameMatrix(e,t.minX,t.minY,t.maxX,t.maxY)}addBoundsArea(t,e){const i=t.minX>e.x?t.minX:e.x,a=t.minY>e.y?t.minY:e.y,o=t.maxXd?o:d,this.maxY=c>m?c:m}}pad(t=0,e=t){this.isEmpty()||(this.minX-=t,this.maxX+=t,this.minY-=e,this.maxY+=e)}addFramePad(t,e,i,a,o,c){t-=o,e-=c,i+=o,a+=c,this.minX=this.minXi?this.maxX:i,this.minY=this.minYa?this.maxY:a}}class Wa extends Eh{constructor(){super(),this.tempDisplayObjectParent=null,this.transform=new M2,this.alpha=1,this.visible=!0,this.renderable=!0,this.cullable=!1,this.cullArea=null,this.parent=null,this.worldAlpha=1,this._lastSortedIndex=0,this._zIndex=0,this.filterArea=null,this.filters=null,this._enabledFilters=null,this._bounds=new Nm,this._localBounds=null,this._boundsID=0,this._boundsRect=null,this._localBoundsRect=null,this._mask=null,this._maskRefCount=0,this._destroyed=!1,this.isSprite=!1,this.isMask=!1}static mixin(t){const e=Object.keys(t);for(let i=0;i1)for(let e=0;ethis.children.length)throw new Error(`${t}addChildAt: The index ${e} supplied is out of bounds ${this.children.length}`);return t.parent&&t.parent.removeChild(t),t.parent=this,this.sortDirty=!0,t.transform._parentID=-1,this.children.splice(e,0,t),this._boundsID++,this.onChildrenChange(e),t.emit("added",this),this.emit("childAdded",t,this,e),t}swapChildren(t,e){if(t===e)return;const i=this.getChildIndex(t),a=this.getChildIndex(e);this.children[i]=e,this.children[a]=t,this.onChildrenChange(i=this.children.length)throw new Error(`The index ${e} supplied is out of bounds ${this.children.length}`);const i=this.getChildIndex(t);qu(this.children,i,1),this.children.splice(e,0,t),this.onChildrenChange(e)}getChildAt(t){if(t<0||t>=this.children.length)throw new Error(`getChildAt: Index (${t}) does not exist.`);return this.children[t]}removeChild(...t){if(t.length>1)for(let e=0;e0&&o<=a){c=this.children.splice(i,o);for(let u=0;u1&&this.children.sort(JL),this.sortDirty=!1}updateTransform(){this.sortableChildren&&this.sortDirty&&this.sortChildren(),this._boundsID++,this.transform.updateTransform(this.parent.transform),this.worldAlpha=this.alpha*this.parent.worldAlpha;for(let t=0,e=this.children.length;t0&&e.height>0))return;let i,a;this.cullArea?(i=this.cullArea,a=this.worldTransform):this._render!==Om.prototype._render&&(i=this.getBounds(!0));const o=t.projection.transform;if(o&&(a?(a=QL.copyFrom(a),a.prepend(o)):a=o),i&&e.intersects(i,a))this._render(t);else if(this.cullArea)return;for(let c=0,u=this.children.length;c{e.init.call(this,t)})}render(){this.renderer.render(this.stage)}get view(){return this.renderer?.view}get screen(){return this.renderer?.screen}destroy(t,e){const i=Im._plugins.slice(0);i.reverse(),i.forEach(a=>{a.destroy.call(this)}),this.stage.destroy(e),this.stage=null,this.renderer.destroy(t),this.renderer=null}};qv._plugins=[];let Xv=qv;ae.handleByList(Ht.Application,Xv._plugins);class Yv{static init(t){Object.defineProperty(this,"resizeTo",{set(e){globalThis.removeEventListener("resize",this.queueResize),this._resizeTo=e,e&&(globalThis.addEventListener("resize",this.queueResize),this.resize())},get(){return this._resizeTo}}),this.queueResize=()=>{this._resizeTo&&(this.cancelResize(),this._resizeId=requestAnimationFrame(()=>this.resize()))},this.cancelResize=()=>{this._resizeId&&(cancelAnimationFrame(this._resizeId),this._resizeId=null)},this.resize=()=>{if(!this._resizeTo)return;this.cancelResize();let e,i;if(this._resizeTo===globalThis.window)e=globalThis.innerWidth,i=globalThis.innerHeight;else{const{clientWidth:a,clientHeight:o}=this._resizeTo;e=a,i=o}this.renderer.resize(e,i),this.render()},this._resizeId=null,this._resizeTo=null,this.resizeTo=t.resizeTo||null}static destroy(){globalThis.removeEventListener("resize",this.queueResize),this.cancelResize(),this.cancelResize=null,this.queueResize=null,this.resizeTo=null,this.resize=null}}Yv.extension=Ht.Application;ae.add(Yv);const tN={5:[.153388,.221461,.250301],7:[.071303,.131514,.189879,.214607],9:[.028532,.067234,.124009,.179044,.20236],11:[.0093,.028002,.065984,.121703,.175713,.198596],13:[.002406,.009255,.027867,.065666,.121117,.174868,.197641],15:[489e-6,.002403,.009246,.02784,.065602,.120999,.174697,.197448]},eN=["varying vec2 vBlurTexCoords[%size%];","uniform sampler2D uSampler;","void main(void)","{"," gl_FragColor = vec4(0.0);"," %blur%","}"].join(` +`);function nN(n){const t=tN[n],e=t.length;let i=eN,a="";const o="gl_FragColor += texture2D(uSampler, vBlurTexCoords[%index%]) * %value%;";let c;for(let u=0;u=e&&(c=n-u-1),f=f.replace("%value%",t[c].toString()),a+=f,a+=` +`}return i=i.replace("%blur%",a),i=i.replace("%size%",n.toString()),i}const iN=` + attribute vec2 aVertexPosition; + + uniform mat3 projectionMatrix; + + uniform float strength; + + varying vec2 vBlurTexCoords[%size%]; + + uniform vec4 inputSize; + uniform vec4 outputFrame; + + vec4 filterVertexPosition( void ) + { + vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; + + return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); + } + + vec2 filterTextureCoord( void ) + { + return aVertexPosition * (outputFrame.zw * inputSize.zw); + } + + void main(void) + { + gl_Position = filterVertexPosition(); + + vec2 textureCoord = filterTextureCoord(); + %blur% + }`;function sN(n,t){const e=Math.ceil(n/2);let i=iN,a="",o;t?o="vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);":o="vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);";for(let c=0;c 0.0) { + coord *= mix(1.0, smoothstep(0.0, radius / distance, percent), strength * 0.75); + } else { + coord *= mix(1.0, pow(percent, 1.0 + strength * 0.75) * radius / distance, 1.0 - percent); + } + } + coord += center * dimensions.xy; + coord /= filterArea.xy; + vec2 clampedCoord = clamp(coord, filterClamp.xy, filterClamp.zw); + vec4 color = texture2D(uSampler, clampedCoord); + if (coord != clampedCoord) { + color *= max(0.0, 1.0 - length(coord - clampedCoord)); + } + + gl_FragColor = color; +} +`;const $v=class extends Wn{constructor(n){super(rN,aN),this.uniforms.dimensions=new Float32Array(2),Object.assign(this,$v.defaults,n)}apply(n,t,e,i){const{width:a,height:o}=t.filterFrame;this.uniforms.dimensions[0]=a,this.uniforms.dimensions[1]=o,n.applyFilter(this,t,e,i)}get radius(){return this.uniforms.radius}set radius(n){this.uniforms.radius=n}get strength(){return this.uniforms.strength}set strength(n){this.uniforms.strength=n}get center(){return this.uniforms.center}set center(n){this.uniforms.center=n}};let vl=$v;vl.defaults={center:[.5,.5],radius:100,strength:1};var oN=`varying vec2 vTextureCoord; +uniform sampler2D uSampler; +uniform float m[20]; +uniform float uAlpha; + +void main(void) +{ + vec4 c = texture2D(uSampler, vTextureCoord); + + if (uAlpha == 0.0) { + gl_FragColor = c; + return; + } + + // Un-premultiply alpha before applying the color matrix. See issue #3539. + if (c.a > 0.0) { + c.rgb /= c.a; + } + + vec4 result; + + result.r = (m[0] * c.r); + result.r += (m[1] * c.g); + result.r += (m[2] * c.b); + result.r += (m[3] * c.a); + result.r += m[4]; + + result.g = (m[5] * c.r); + result.g += (m[6] * c.g); + result.g += (m[7] * c.b); + result.g += (m[8] * c.a); + result.g += m[9]; + + result.b = (m[10] * c.r); + result.b += (m[11] * c.g); + result.b += (m[12] * c.b); + result.b += (m[13] * c.a); + result.b += m[14]; + + result.a = (m[15] * c.r); + result.a += (m[16] * c.g); + result.a += (m[17] * c.b); + result.a += (m[18] * c.a); + result.a += m[19]; + + vec3 rgb = mix(c.rgb, result.rgb, uAlpha); + + // Premultiply alpha again. + rgb *= result.a; + + gl_FragColor = vec4(rgb, result.a); +} +`;class wl extends Wn{constructor(){const t={m:new Float32Array([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0]),uAlpha:1};super(jL,oN,t),this.alpha=1}_loadMatrix(t,e=!1){let i=t;e&&(this._multiply(i,this.uniforms.m,t),i=this._colorMatrix(i)),this.uniforms.m=i}_multiply(t,e,i){return t[0]=e[0]*i[0]+e[1]*i[5]+e[2]*i[10]+e[3]*i[15],t[1]=e[0]*i[1]+e[1]*i[6]+e[2]*i[11]+e[3]*i[16],t[2]=e[0]*i[2]+e[1]*i[7]+e[2]*i[12]+e[3]*i[17],t[3]=e[0]*i[3]+e[1]*i[8]+e[2]*i[13]+e[3]*i[18],t[4]=e[0]*i[4]+e[1]*i[9]+e[2]*i[14]+e[3]*i[19]+e[4],t[5]=e[5]*i[0]+e[6]*i[5]+e[7]*i[10]+e[8]*i[15],t[6]=e[5]*i[1]+e[6]*i[6]+e[7]*i[11]+e[8]*i[16],t[7]=e[5]*i[2]+e[6]*i[7]+e[7]*i[12]+e[8]*i[17],t[8]=e[5]*i[3]+e[6]*i[8]+e[7]*i[13]+e[8]*i[18],t[9]=e[5]*i[4]+e[6]*i[9]+e[7]*i[14]+e[8]*i[19]+e[9],t[10]=e[10]*i[0]+e[11]*i[5]+e[12]*i[10]+e[13]*i[15],t[11]=e[10]*i[1]+e[11]*i[6]+e[12]*i[11]+e[13]*i[16],t[12]=e[10]*i[2]+e[11]*i[7]+e[12]*i[12]+e[13]*i[17],t[13]=e[10]*i[3]+e[11]*i[8]+e[12]*i[13]+e[13]*i[18],t[14]=e[10]*i[4]+e[11]*i[9]+e[12]*i[14]+e[13]*i[19]+e[14],t[15]=e[15]*i[0]+e[16]*i[5]+e[17]*i[10]+e[18]*i[15],t[16]=e[15]*i[1]+e[16]*i[6]+e[17]*i[11]+e[18]*i[16],t[17]=e[15]*i[2]+e[16]*i[7]+e[17]*i[12]+e[18]*i[17],t[18]=e[15]*i[3]+e[16]*i[8]+e[17]*i[13]+e[18]*i[18],t[19]=e[15]*i[4]+e[16]*i[9]+e[17]*i[14]+e[18]*i[19]+e[19],t}_colorMatrix(t){const e=new Float32Array(t);return e[4]/=255,e[9]/=255,e[14]/=255,e[19]/=255,e}brightness(t,e){const i=[t,0,0,0,0,0,t,0,0,0,0,0,t,0,0,0,0,0,1,0];this._loadMatrix(i,e)}tint(t,e){const[i,a,o]=Or.shared.setValue(t).toArray(),c=[i,0,0,0,0,0,a,0,0,0,0,0,o,0,0,0,0,0,1,0];this._loadMatrix(c,e)}greyscale(t,e){const i=[t,t,t,0,0,t,t,t,0,0,t,t,t,0,0,0,0,0,1,0];this._loadMatrix(i,e)}blackAndWhite(t){const e=[.3,.6,.1,0,0,.3,.6,.1,0,0,.3,.6,.1,0,0,0,0,0,1,0];this._loadMatrix(e,t)}hue(t,e){t=(t||0)/180*Math.PI;const i=Math.cos(t),a=Math.sin(t),o=Math.sqrt,c=1/3,u=o(c),f=i+(1-i)*c,d=c*(1-i)-u*a,m=c*(1-i)+u*a,g=c*(1-i)+u*a,v=i+c*(1-i),x=c*(1-i)-u*a,b=c*(1-i)-u*a,C=c*(1-i)+u*a,T=i+c*(1-i),S=[f,d,m,0,0,g,v,x,0,0,b,C,T,0,0,0,0,0,1,0];this._loadMatrix(S,e)}contrast(t,e){const i=(t||0)+1,a=-.5*(i-1),o=[i,0,0,0,a,0,i,0,0,a,0,0,i,0,a,0,0,0,1,0];this._loadMatrix(o,e)}saturate(t=0,e){const i=t*2/3+1,a=(i-1)*-.5,o=[i,a,a,0,0,a,i,a,0,0,a,a,i,0,0,0,0,0,1,0];this._loadMatrix(o,e)}desaturate(){this.saturate(-1)}negative(t){const e=[-1,0,0,1,0,0,-1,0,1,0,0,0,-1,1,0,0,0,0,1,0];this._loadMatrix(e,t)}sepia(t){const e=[.393,.7689999,.18899999,0,0,.349,.6859999,.16799999,0,0,.272,.5339999,.13099999,0,0,0,0,0,1,0];this._loadMatrix(e,t)}technicolor(t){const e=[1.9125277891456083,-.8545344976951645,-.09155508482755585,0,11.793603434377337,-.3087833385928097,1.7658908555458428,-.10601743074722245,0,-70.35205161461398,-.231103377548616,-.7501899197440212,1.847597816108189,0,30.950940869491138,0,0,0,1,0];this._loadMatrix(e,t)}polaroid(t){const e=[1.438,-.062,-.062,0,0,-.122,1.378,-.122,0,0,-.016,-.016,1.483,0,0,0,0,0,1,0];this._loadMatrix(e,t)}toBGR(t){const e=[0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0];this._loadMatrix(e,t)}kodachrome(t){const e=[1.1285582396593525,-.3967382283601348,-.03992559172921793,0,63.72958762196502,-.16404339962244616,1.0835251566291304,-.05498805115633132,0,24.732407896706203,-.16786010706155763,-.5603416277695248,1.6014850761964943,0,35.62982807460946,0,0,0,1,0];this._loadMatrix(e,t)}browni(t){const e=[.5997023498159715,.34553243048391263,-.2708298674538042,0,47.43192855600873,-.037703249837783157,.8609577587992641,.15059552388459913,0,-36.96841498319127,.24113635128153335,-.07441037908422492,.44972182064877153,0,-7.562075277591283,0,0,0,1,0];this._loadMatrix(e,t)}vintage(t){const e=[.6279345635605994,.3202183420819367,-.03965408211312453,0,9.651285835294123,.02578397704808868,.6441188644374771,.03259127616149294,0,7.462829176470591,.0466055556782719,-.0851232987247891,.5241648018700465,0,5.159190588235296,0,0,0,1,0];this._loadMatrix(e,t)}colorTone(t,e,i,a,o){t=t||.2,e=e||.15,i=i||16770432,a=a||3375104;const c=Or.shared,[u,f,d]=c.setValue(i).toArray(),[m,g,v]=c.setValue(a).toArray(),x=[.3,.59,.11,0,0,u,f,d,t,0,m,g,v,e,0,u-m,f-g,d-v,0,0];this._loadMatrix(x,o)}night(t,e){t=t||.1;const i=[t*-2,-t,0,0,0,-t,0,t,0,0,0,t,t*2,0,0,0,0,0,1,0];this._loadMatrix(i,e)}predator(t,e){const i=[11.224130630493164*t,-4.794486999511719*t,-2.8746118545532227*t,0*t,.40342438220977783*t,-3.6330697536468506*t,9.193157196044922*t,-2.951810836791992*t,0*t,-1.316135048866272*t,-3.2184197902679443*t,-4.2375030517578125*t,7.476448059082031*t,0*t,.8044459223747253*t,0,0,0,1,0];this._loadMatrix(i,e)}lsd(t){const e=[2,-.4,.5,0,0,-.5,2,-.4,0,0,-.4,-.5,3,0,0,0,0,0,1,0];this._loadMatrix(e,t)}reset(){const t=[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0];this._loadMatrix(t,!1)}get matrix(){return this.uniforms.m}set matrix(t){this.uniforms.m=t}get alpha(){return this.uniforms.uAlpha}set alpha(t){this.uniforms.uAlpha=t}}wl.prototype.grayscale=wl.prototype.greyscale;const rl=new an,lN=new Uint16Array([0,1,2,0,2,3]);class Va extends Ir{constructor(t){super(),this._anchor=new Fs(this._onAnchorUpdate,this,t?t.defaultAnchor.x:0,t?t.defaultAnchor.y:0),this._texture=null,this._width=0,this._height=0,this._tintColor=new Or(16777215),this._tintRGB=null,this.tint=16777215,this.blendMode=Nt.NORMAL,this._cachedTint=16777215,this.uvs=null,this.texture=t||ne.EMPTY,this.vertexData=new Float32Array(8),this.vertexTrimmedData=null,this._transformID=-1,this._textureID=-1,this._transformTrimmedID=-1,this._textureTrimmedID=-1,this.indices=lN,this.pluginName="batch",this.isSprite=!0,this._roundPixels=Kt.ROUND_PIXELS}_onTextureUpdate(){this._textureID=-1,this._textureTrimmedID=-1,this._cachedTint=16777215,this._width&&(this.scale.x=xu(this.scale.x)*this._width/this._texture.orig.width),this._height&&(this.scale.y=xu(this.scale.y)*this._height/this._texture.orig.height)}_onAnchorUpdate(){this._transformID=-1,this._transformTrimmedID=-1}calculateVertices(){const t=this._texture;if(this._transformID===this.transform._worldID&&this._textureID===t._updateID)return;this._textureID!==t._updateID&&(this.uvs=this._texture._uvs.uvsFloat32),this._transformID=this.transform._worldID,this._textureID=t._updateID;const e=this.transform.worldTransform,i=e.a,a=e.b,o=e.c,c=e.d,u=e.tx,f=e.ty,d=this.vertexData,m=t.trim,g=t.orig,v=this._anchor;let x=0,b=0,C=0,T=0;if(m?(b=m.x-v._x*g.width,x=b+m.width,T=m.y-v._y*g.height,C=T+m.height):(b=-v._x*g.width,x=b+g.width,T=-v._y*g.height,C=T+g.height),d[0]=i*b+o*T+u,d[1]=c*T+a*b+f,d[2]=i*x+o*T+u,d[3]=c*T+a*x+f,d[4]=i*x+o*C+u,d[5]=c*C+a*x+f,d[6]=i*b+o*C+u,d[7]=c*C+a*b+f,this._roundPixels){const S=Kt.RESOLUTION;for(let L=0;L=a&&rl.x=o&&rl.y{switch(n){case"Function":case"SharedWorker":case"Worker":case"eval":case"setInterval":case"setTimeout":throw new TypeError("unable to deserialize "+n)}return new Qv[n](t)},cN=(n,t)=>{const e=(a,o)=>(n.set(o,a),a),i=a=>{if(n.has(a))return n.get(a);const[o,c]=t[a];switch(o){case Nh:case Wv:return e(c,a);case Ml:{const u=e([],a);for(const f of c)u.push(i(f));return u}case gh:{const u=e({},a);for(const[f,d]of c)u[i(f)]=i(d);return u}case N2:return e(new Date(c),a);case O2:{const{source:u,flags:f}=c;return e(new RegExp(u,f),a)}case I2:{const u=e(new Map,a);for(const[f,d]of c)u.set(i(f),i(d));return u}case D2:{const u=e(new Set,a);for(const f of c)u.add(i(f));return u}case Zv:{const{name:u,message:f}=c;return e(typeof Qv[u]=="function"?m8(u,f):new Error(f),a)}case Kv:return e(BigInt(c),a);case"BigInt":return e(Object(BigInt(c)),a);case"ArrayBuffer":return e(new Uint8Array(c).buffer,c);case"DataView":{const{buffer:u}=new Uint8Array(c);return e(new DataView(u),c)}}return e(m8(o,c),a)};return i},p8=n=>cN(new Map,n)(0),Cr="",{toString:uN}={},{keys:hN}=Object,al=n=>{const t=typeof n;if(t!=="object"||!n)return[Nh,t];const e=uN.call(n).slice(8,-1);switch(e){case"Array":return[Ml,Cr];case"Object":return[gh,Cr];case"Date":return[N2,Cr];case"RegExp":return[O2,Cr];case"Map":return[I2,Cr];case"Set":return[D2,Cr];case"DataView":return[Ml,e]}return e.includes("Array")?[Ml,e]:n instanceof Error?[Zv,n.name||"Error"]:[gh,e]},Lu=([n,t])=>n===Nh&&(t==="function"||t==="symbol"),fN=(n,t,e,i)=>{const a=(c,u)=>{const f=i.push(c)-1;return e.set(u,f),f},o=c=>{if(e.has(c))return e.get(c);let[u,f]=al(c);switch(u){case Nh:{let m=c;switch(f){case"bigint":u=Kv,m=c.toString();break;case"function":case"symbol":if(n)throw new TypeError("unable to serialize "+f);m=null;break;case"undefined":return a([Wv],c)}return a([u,m],c)}case Ml:{if(f){let v=c;return f==="DataView"?v=new Uint8Array(c.buffer):f==="ArrayBuffer"&&(v=new Uint8Array(c)),a([f,[...v]],c)}const m=[],g=a([u,m],c);for(const v of c)m.push(o(v));return g}case gh:{if(f)switch(f){case"BigInt":return a([f,c.toString()],c);case"Boolean":case"Number":case"String":return a([f,c.valueOf()],c)}if(t&&"toJSON"in c)return o(c.toJSON());const m=[],g=a([u,m],c);for(const v of hN(c))(n||!Lu(al(c[v])))&&m.push([o(v),o(c[v])]);return g}case N2:return a([u,isNaN(c.getTime())?Cr:c.toISOString()],c);case O2:{const{source:m,flags:g}=c;return a([u,{source:m,flags:g}],c)}case I2:{const m=[],g=a([u,m],c);for(const[v,x]of c)(n||!(Lu(al(v))||Lu(al(x))))&&m.push([o(v),o(x)]);return g}case D2:{const m=[],g=a([u,m],c);for(const v of c)(n||!Lu(al(v)))&&m.push(o(v));return g}}const{message:d}=c;return a([u,{name:f,message:d}],c)};return o},g8=(n,{json:t,lossy:e}={})=>{const i=[];return fN(!(t||e),!!t,new Map,i)(n),i},Dm=typeof structuredClone=="function"?(n,t)=>t&&("json"in t||"lossy"in t)?p8(g8(n,t)):structuredClone(n):(n,t)=>p8(g8(n,t));function y8(n){return n}const{cbrt:N0,sqrt:O0,PI:Ea}=Math,dN=(n,t,e,i,a)=>{const o=t+e*n,c=o**2+i;if(c>0){const m=O0(c);return N0(o+m)+N0(o-m)-a}const u=N0(O0(o*o-c)),f=o?Math.atan(O0(-c)/o):-Ea/2;let d;return e<0?d=(o>0?2*Ea:Ea)-f:a<0?d=(o>0?2*Ea:-3*Ea)+f:d=(o>0?0:Ea)+f,2*u*Math.cos(d/3)-a},mN=(n,t,e,i)=>((t*n+3*e)*n+i)*n;function Jv(n,t,e,i){if(!(0<=n&&n<=1&&0<=e&&e<=1))throw new Error("bezier x values must be in [0, 1] range");if(n===t&&e===i)return y8;const a=6*(3*n-3*e+1),o=6*(e-2*n),c=3*n,u=a*a,f=o*o,d=o/a,m=3*o*c/u-f*o/(u*a),g=2*c/a-f/u,v=g*g*g,x=3/a,b=3*t-3*i+1,C=i-2*t,T=3*t,S=a?dN:y8;return function(R){return R===0||R===1?R:mN(S(R,m,x,v,d),b,C,T)}}var pN=class{};function v8(n){return Math.max(1,n)}var tb=class extends pN{canvas;observer;flowSpeed=1;currerntRenderScale=.75;constructor(n){super(),this.canvas=n,this.observer=new ResizeObserver(()=>{const t=v8(n.clientWidth*window.devicePixelRatio*this.currerntRenderScale),e=v8(n.clientHeight*window.devicePixelRatio*this.currerntRenderScale);this.onResize(t,e)}),this.observer.observe(n)}setRenderScale(n){this.currerntRenderScale=n,this.onResize(this.canvas.clientWidth*window.devicePixelRatio*this.currerntRenderScale,this.canvas.clientHeight*window.devicePixelRatio*this.currerntRenderScale)}onResize(n,t){this.canvas.width=n,this.canvas.height=t}setFlowSpeed(n){this.flowSpeed=n}dispose(){this.observer.disconnect(),this.canvas.remove()}getElement(){return this.canvas}};function gN(n){return new Promise((t,e)=>{const i=document.createElement("img");i.onload=()=>t(i),i.onerror=e,i.src=n,i.crossOrigin="anonymous",i.loading="eager"})}function yN(n){return new Promise((t,e)=>{const i=document.createElement("video");let a=!1,o=!1,c=!1;i.addEventListener("playing",()=>{a=!0,u()},!0),i.addEventListener("timeupdate",()=>{o=!0,u()},!0),i.addEventListener("error",f=>{c=!0,e(f)},!0);function u(){a&&o&&!c&&t(i)}i.src=n,i.playsInline=!0,i.crossOrigin="anonymous",i.autoplay=!0,i.loop=!0,i.muted=!0,i.play()})}function Bm(n,t=!1){return t?yN(n):gN(n)}function eb(n){return new Promise((t,e)=>{(n instanceof HTMLImageElement?n.complete:n.readyState>=3)?t(n):(n.onload=()=>t(n),n.onerror=e)})}function vN(n,t,e){const i=n.data,a=n.width,o=n.height;let c,u,f,d,m,g,v,x,b,C,T,S,L;const R=a-1,P=o-1,w=t+1,F=t+w,E=t+1,O=1/(F*(t+E)),I=[],k=[],U=[],G=[],J=[],tt=[];for(;e-- >0;){for(L=S=0,g=0;gR?R:v)<<2),c+=i[x++],u+=i[x++],f+=i[x++],d+=i[x];for(m=0;mP?0:a,c+=I[T],u+=k[T],f+=U[T],d+=G[T];for(S=m<<2,g=0;gObject.freeze({cx:n,cy:t,x:e,y:i,ur:a,vr:o,up:c,vp:u}),Sr=(n,t,e)=>Object.freeze({width:n,height:t,conf:e}),b8=[Sr(5,5,[ot(0,0,-1,-1,0,0,1,1),ot(1,0,-.5,-1,0,0,1,1),ot(2,0,0,-1,0,0,1,1),ot(3,0,.5,-1,0,0,1,1),ot(4,0,1,-1,0,0,1,1),ot(0,1,-1,-.5,0,0,1,1),ot(1,1,-.5,-.5,0,0,1,1),ot(2,1,-.0052029684413368305,-.6131420587090777,0,0,1,1),ot(3,1,.5884227308309977,-.3990805107556692,0,0,1,1),ot(4,1,1,-.5,0,0,1,1),ot(0,2,-1,0,0,0,1,1),ot(1,2,-.4210024670505933,-.11895058380429502,0,0,1,1),ot(2,2,-.1019613423315412,-.023812118047224606,0,-47,.629,.849),ot(3,2,.40275125660925437,-.06345314544600389,0,0,1,1),ot(4,2,1,0,0,0,1,1),ot(0,3,-1,.5,0,0,1,1),ot(1,3,.06801958477287173,.5205913248960121,-31,-45,1,1),ot(2,3,.21446469120128908,.29331610114301043,6,-56,.566,1.321),ot(3,3,.5,.5,0,0,1,1),ot(4,3,1,.5,0,0,1,1),ot(0,4,-1,1,0,0,1,1),ot(1,4,-.31378372841550195,1,0,0,1,1),ot(2,4,.26153633255328046,1,0,0,1,1),ot(3,4,.5,1,0,0,1,1),ot(4,4,1,1,0,0,1,1)]),Sr(4,4,[ot(0,0,-1,-1,0,0,1,1),ot(1,0,-.33333333333333337,-1,0,0,1,1),ot(2,0,.33333333333333326,-1,0,0,1,1),ot(3,0,1,-1,0,0,1,1),ot(0,1,-1,-.04495399932657351,0,0,1,1),ot(1,1,-.24056117520129328,-.22465999020104,0,0,1,1),ot(2,1,.334758885767489,-.00531297192779423,0,0,1,1),ot(3,1,.9989920470678106,-.3382976020775408,8,0,.566,1.792),ot(0,2,-1,.33333333333333326,0,0,1,1),ot(1,2,-.3425497314639411,-27501607956947893e-21,0,0,1,1),ot(2,2,.3321437945812673,.1981776353859399,0,0,1,1),ot(3,2,1,.0766118180296832,0,0,1,1),ot(0,3,-1,1,0,0,1,1),ot(1,3,-.33333333333333337,1,0,0,1,1),ot(2,3,.33333333333333326,1,0,0,1,1),ot(3,3,1,1,0,0,1,1)]),Sr(4,4,[ot(0,0,-1,-1,0,0,1,2.075),ot(1,0,-.33333333333333337,-1,0,0,1,1),ot(2,0,.33333333333333326,-1,0,0,1,1),ot(3,0,1,-1,0,0,1,1),ot(0,1,-1,-.4545779491139603,0,0,1,1),ot(1,1,-.33333333333333337,-.33333333333333337,0,0,1,1),ot(2,1,.0889403142626457,-.6025711180694033,-32,45,1,1),ot(3,1,1,-.33333333333333337,0,0,1,1),ot(0,2,-1,-.07402408608567845,1,0,1,.094),ot(1,2,-.2719422694359541,.09775369930903222,25,-18,1.321,0),ot(2,2,.19877414408395877,.4307383294587789,48,-40,.755,.975),ot(3,2,1,.33333333333333326,-37,0,1,1),ot(0,3,-1,1,0,0,1,1),ot(1,3,-.33333333333333337,1,0,0,1,1),ot(2,3,.5125850864305672,1,-20,-18,0,1.604),ot(3,3,1,1,0,0,1,1)]),Sr(5,5,[ot(0,0,-1,-1,0,0,1,1),ot(1,0,-.4501953125,-1,0,55,1,2.075),ot(2,0,.1953125,-1,0,0,1,1),ot(3,0,.4580078125,-1,0,-25,1,1),ot(4,0,1,-1,0,0,1,1),ot(0,1,-1,-.2514475377525607,-16,0,2.327,.943),ot(1,1,-.55859375,-.6609325945787148,47,0,2.358,.377),ot(2,1,.232421875,-.5244375756366635,-66,-25,1.855,1.164),ot(3,1,.685546875,-.3753706470552125,0,0,1,1),ot(4,1,1,-.6699125300354287,0,0,1,1),ot(0,2,-1,.035910396862284255,0,0,1,1),ot(1,2,-.4921875,.005378616309457018,90,23,1,1.981),ot(2,2,.021484375,-.1365043639066228,0,42,1,1),ot(3,2,.4765625,.05925822904974043,-30,0,1.95,.44),ot(4,2,1,.251428847823418,0,0,1,1),ot(0,3,-1,.6968336464764276,-68,0,1,.786),ot(1,3,-.6904296875,.5890744209958608,-68,0,1,1),ot(2,3,.1845703125,.3879238667654693,61,0,1,1),ot(3,3,.60546875,.4633553246018661,-47,-59,.849,1.73),ot(4,3,1,.6214021886400309,-33,0,.377,1.604),ot(0,4,-1,1,0,0,1,1),ot(1,4,-.5,1,0,-73,1,1),ot(2,4,-.3271484375,1,0,-24,.314,2.704),ot(3,4,.5,1,0,0,1,1),ot(4,4,1,1,0,0,1,1)]),Sr(5,5,[ot(0,0,-1,-1),ot(1,0,-.6393,-1,0,0,1,2.3884),ot(2,0,0,-1),ot(3,0,.5,-1),ot(4,0,1,-1),ot(0,1,-1,-.2301),ot(1,1,-.6934,-.331,0,-.7188,1,1.063),ot(2,1,-.0082,-.6814,-.2583,0,1.0964,1),ot(3,1,.5836,-.531,.7029,0,1.5466,1),ot(4,1,1,-.6407),ot(0,2,-1,.2973,0,0,1.8352,1),ot(1,2,-.4082,.0602),ot(2,2,-.1803,-.3646,-.2998,0,1.1513,1),ot(3,2,.477,-.1027,.8903,-.1882,1.0807,.8551),ot(4,2,1,-.2973),ot(0,3,-1,.7628,0,0,2.3868,1),ot(1,3,-.2525,.4814,-.8406,-1.6199,1.4093,1.2215),ot(2,3,.3607,.2814,-1.0713,-.0529,1.0025,.7611),ot(3,3,.4885,.623,0,.8184,1,1.2876),ot(4,3,1,.5),ot(0,4,-1,1),ot(1,4,-.4033,1),ot(2,4,.2672,1),ot(3,4,.5967,1),ot(4,4,1,1)]),Sr(5,5,[ot(0,0,-1,-1),ot(1,0,-.2197,-1),ot(2,0,.0197,-1),ot(3,0,.8033,-1),ot(4,0,1,-1),ot(0,1,-1,-.5451),ot(1,1,-.4885,-.4035,-1.0246,-.2268,1.1936,.8005),ot(2,1,-.1213,-.2867,0,-.6981,1,.809),ot(3,1,.3246,-.5628,0,-1.2188,1,1.044),ot(4,1,1,-.3292),ot(0,2,-1,.1416),ot(1,2,-.341,-.0142,0,-.4004,1,1.1293),ot(2,2,-.0393,-.023,.2915,-.373,1.044,.9879),ot(3,2,.3148,-.0673,-.7853,-.8962,1.4709,1.0247),ot(4,2,1,.1912),ot(0,3,-1,.5),ot(1,3,-.2689,.2743,.3404,-.5248,1.0184,.4391),ot(2,3,.0721,.269,.5302,.1244,.6723,.3225),ot(3,3,.4148,.3894,-.6977,-.6783,.8094,.9247),ot(4,3,1,.446),ot(0,4,-1,1),ot(1,4,-.7311,1),ot(2,4,.323,1),ot(3,4,.6393,1),ot(4,4,1,1)])],ri=(n,t)=>Math.random()*(t-n)+n;function bN(n,t,e){const i=li((e-n)/(t-n));return i*i*(3-2*i)}function xN(n,t,e,i=2,a=.5,o=.1){let c=[],u=a;for(let m=0;m 1.0) { + pos.y *= u_aspect; + } else { + pos.x /= u_aspect; + } + gl_Position = vec4(pos, 0.0, 1.0); +} +`;const wN=` +attribute vec2 a_pos; +varying vec2 v_uv; +void main() { + gl_Position = vec4(a_pos, 0.0, 1.0); + v_uv = a_pos * 0.5 + 0.5; +} +`,MN=` +precision mediump float; +varying vec2 v_uv; +uniform sampler2D u_texture; +uniform float u_alpha; +void main() { + vec4 color = texture2D(u_texture, v_uv); + gl_FragColor = vec4(color.rgb, color.a * u_alpha); +} +`;function RN(n){return-(Math.cos(Math.PI*n)-1)/2}var x8=class{label;gl;program;vertexShader;fragmentShader;attrs;constructor(n,t,e,i="unknown"){this.label=i,this.gl=n,this.vertexShader=this.createShader(n.VERTEX_SHADER,t),this.fragmentShader=this.createShader(n.FRAGMENT_SHADER,e),this.program=this.createProgram();const a=n.getProgramParameter(this.program,n.ACTIVE_ATTRIBUTES),o={};for(let c=0;c=this.vertexData.length-1){console.warn("Vertex position out of range",a,this.vertexData.length);return}this.vertexData[a]=e,this.vertexData[a+1]=i}setVertexColor(n,t,e,i,a){const o=(n+t*this.vertexWidth)*7+2;if(o>=this.vertexData.length-2){console.warn("Vertex color out of range",o,this.vertexData.length);return}this.vertexData[o]=e,this.vertexData[o+1]=i,this.vertexData[o+2]=a}setVertexUV(n,t,e,i){const a=(n+t*this.vertexWidth)*7+5;if(a>=this.vertexData.length-1){console.warn("Vertex UV out of range",a,this.vertexData.length);return}this.vertexData[a]=e,this.vertexData[a+1]=i}setVertexData(n,t,e,i,a,o,c,u,f){const d=(n+t*this.vertexWidth)*7;if(d>=this.vertexData.length-6){console.warn("Vertex data out of range",d,this.vertexData.length);return}const m=this.vertexData;m[d]=e,m[d+1]=i,m[d+2]=a,m[d+3]=o,m[d+4]=c,m[d+5]=u,m[d+6]=f}getVertexIndexLength(){return this.vertexIndexLength}draw(){const n=this.gl;this.wireFrame?n.drawElements(n.LINES,this.vertexIndexLength,n.UNSIGNED_SHORT,0):n.drawElements(n.TRIANGLES,this.vertexIndexLength,n.UNSIGNED_SHORT,0)}resize(n,t){this.vertexWidth=n,this.vertexHeight=t,this.vertexIndexLength=n*t*6,this.wireFrame&&(this.vertexIndexLength=n*t*10);const e=new Float32Array(n*t*7),i=new Uint16Array(this.vertexIndexLength);this.vertexData=e,this.indexData=i;for(let o=0;od.location[a],u=d=>d.uTangent[a],f=d=>d.vTangent[a];return o[0]=c(n),o[1]=c(t),o[2]=f(n),o[3]=f(t),o[4]=c(e),o[5]=c(i),o[6]=f(e),o[7]=f(i),o[8]=u(n),o[9]=u(t),o[10]=0,o[11]=0,o[12]=u(e),o[13]=u(i),o[14]=0,o[15]=0,o}function I0(n,t,e,i,a,o=Rt.create()){const c=u=>u.color[a];return o.fill(0),o[0]=c(n),o[1]=c(t),o[4]=c(e),o[5]=c(i),o}var IN=class{_width=0;_height=0;_data=[];constructor(n,t){this.resize(n,t),Object.seal(this)}resize(n,t){this._width=n,this._height=t,this._data=new Array(n*t).fill(0)}set(n,t,e){this._data[n+t*this._width]=e}get(n,t){return this._data[n+t*this._width]}get width(){return this._width}get height(){return this._height}},DN=class extends LN{_subDivisions=10;_controlPoints=new IN(3,3);constructor(n,t,e,i){super(n,t,e,i),this.resizeControlPoints(3,3),Object.seal(this)}setWireFrame(n){super.setWireFrame(n),this.updateMesh()}resetSubdivition(n){this._subDivisions=n,super.resize((this._controlPoints.width-1)*n,(this._controlPoints.height-1)*n)}resizeControlPoints(n,t){if(!(n>=2&&t>=2))throw new Error("Control points must be larger than 3x3 or equal");this._controlPoints.resize(n,t);for(let e=0;e0&&t>0&&this.updateFBO(n,t)}}updateFBO(n,t){const e=this.gl;this.fbo&&e.deleteFramebuffer(this.fbo),this.fboTexture&&e.deleteTexture(this.fboTexture),this.fboTexture=e.createTexture(),e.bindTexture(e.TEXTURE_2D,this.fboTexture),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,t,0,e.RGBA,e.UNSIGNED_BYTE,null),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),this.fbo=e.createFramebuffer(),e.bindFramebuffer(e.FRAMEBUFFER,this.fbo),e.framebufferTexture2D(e.FRAMEBUFFER,e.COLOR_ATTACHMENT0,e.TEXTURE_2D,this.fboTexture,0),e.bindFramebuffer(e.FRAMEBUFFER,null)}onRedraw(n,t){const e=this.meshStates[this.meshStates.length-1];let i=!1;const a=t/500;if(e)if(e.mesh.bind(),this.manualControl&&e.mesh.updateMesh(),this.isNoCover){let u=!1;for(let f=this.meshStates.length-1;f>=0;f--){const d=this.meshStates[f];d.alpha<=-.1?(d.mesh.dispose(),d.texture.dispose(),this.meshStates.splice(f,1)):(d.alpha=Math.max(-.1,d.alpha-a),u=!0)}i=!u}else{if(e.alpha>=1.1){const u=this.meshStates.splice(0,this.meshStates.length-1);for(const f of u)f.mesh.dispose(),f.texture.dispose()}else e.alpha=Math.min(1.1,e.alpha+a);i=this.meshStates.length===1&&e.alpha>=1.1}const o=this.gl;if(this.checkIfResize(),!this.fbo)return i;o.bindFramebuffer(o.FRAMEBUFFER,null),o.clearColor(0,0,0,0),o.clear(o.COLOR_BUFFER_BIT);const c=Math.min(1,t/100);this.smoothedVolume+=(this.volume-this.smoothedVolume)*c;for(const u of this.meshStates){o.bindFramebuffer(o.FRAMEBUFFER,this.fbo),o.disable(o.BLEND),o.clearColor(0,0,0,0),o.clear(o.COLOR_BUFFER_BIT),this.mainProgram.use(),o.activeTexture(o.TEXTURE0);const f=n/1e4;this.mainProgram.setUniform1f("u_aspect",this.manualControl?1:this.canvas.width/this.canvas.height),this.mainProgram.setUniform1i("u_texture",0),this.mainProgram.setUniform1f("u_volume",this.volume),this.mainProgram.setUniform1f("u_alpha",1);const d=(f+this.volume)*2;this.mainProgram.setUniform1f("u_sinAngle",Math.sin(d)),this.mainProgram.setUniform1f("u_cosAngle",Math.cos(d)),u.texture.bind(),u.mesh.bind(),u.mesh.draw(),o.bindFramebuffer(o.FRAMEBUFFER,null),o.enable(o.BLEND),o.blendFuncSeparate(o.SRC_ALPHA,o.ONE_MINUS_SRC_ALPHA,o.ONE,o.ONE_MINUS_SRC_ALPHA),this.quadProgram.use(),this.quadProgram.setUniform1i("u_texture",0),this.quadProgram.setUniform1f("u_alpha",RN(li(u.alpha))),o.activeTexture(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,this.fboTexture),o.bindBuffer(o.ARRAY_BUFFER,this.quadBuffer);const m=this.quadProgram.attrs.a_pos;o.vertexAttribPointer(m,2,o.FLOAT,!1,0,0),o.enableVertexAttribArray(m),o.drawArrays(o.TRIANGLES,0,6),o.disableVertexAttribArray(m)}return o.flush(),i}onTickBinded=this.onTick.bind(this);requestTick(){this._disposed||this.tickHandle===0&&(this.tickHandle=requestAnimationFrame(this.onTickBinded))}constructor(n){super(n);const t=n.getContext("webgl",{antialias:!0});if(!t)throw new Error("WebGL not supported");t.getExtension("EXT_color_buffer_float")||console.warn("EXT_color_buffer_float not supported"),t.getExtension("EXT_float_blend")||console.warn("EXT_float_blend not supported"),t.getExtension("OES_texture_float_linear")||console.warn("OES_texture_float_linear not supported"),t.getExtension("OES_texture_float")||console.warn("OES_texture_float not supported"),this.gl=t,t.enable(t.BLEND),t.blendFuncSeparate(t.SRC_ALPHA,t.ONE_MINUS_SRC_ALPHA,t.ONE,t.ONE_MINUS_SRC_ALPHA),t.enable(t.DEPTH_TEST),t.depthFunc(t.ALWAYS),this.mainProgram=new x8(t,EN,AN,"main-program-mg"),this.quadProgram=new x8(t,wN,MN,"quad-program");const e=t.createBuffer();if(!e)throw new Error("Failed to create quad buffer");this.quadBuffer=e,t.bindBuffer(t.ARRAY_BUFFER,this.quadBuffer),t.bufferData(t.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),t.STATIC_DRAW),this.requestTick()}onResize(n,t){this.targetSize.x=Math.ceil(n),this.targetSize.y=Math.ceil(t),this.requestTick()}setStaticMode(n){this.staticMode=n,this.lastFrameTime=performance.now(),this.requestTick()}setFPS(n){this.maxFPS=n}pause(){this.tickHandle&&(cancelAnimationFrame(this.tickHandle),this.tickHandle=0),this.paused=!0}resume(){this.paused=!1,this.requestTick()}async setAlbum(n,t){if(n===void 0||typeof n=="string"&&n.trim().length===0){this.isNoCover=!0;return}let e=null,i=null,a=5;for(;!e&&a>0;)try{typeof n=="string"?!t&&"createImageBitmap"in window?(i=await(await fetch(n)).blob(),e=await Bm(URL.createObjectURL(i),!1)):e=await Bm(n,t):e=await eb(n)}catch(v){console.warn(`failed on loading album resource, retrying (${a})`,{albumSource:n,error:v}),a--}if(!e){console.error("Failed to load album resource",n),this.isNoCover=!0;return}this.isNoCover=!1;const o=this.reduceImageSizeCanvas,c=o.getContext("2d",{willReadFrequently:!0});if(!c)throw new Error("Failed to create canvas context");c.clearRect(0,0,o.width,o.height);const u=e instanceof HTMLVideoElement?e.videoWidth:e.naturalWidth,f=e instanceof HTMLVideoElement?e.videoHeight:e.naturalHeight;if(u*f===0)throw new Error("Invalid image size");let d=null;try{"createImageBitmap"in window&&(i?(d=await createImageBitmap(i,{resizeWidth:o.width,resizeHeight:o.height,resizeQuality:"low"}),URL.revokeObjectURL(e.src)):d=await createImageBitmap(e,{resizeWidth:o.width,resizeHeight:o.height,resizeQuality:"low"}))}catch(v){console.warn("createImageBitmap failed",v)}d?(c.drawImage(d,0,0),d.close()):c.drawImage(e,0,0,u,f,0,0,o.width,o.height);const m=c.getImageData(0,0,o.width,o.height),g=m.data;for(let v=0;v0)this.meshStates[0].texture.dispose(),this.meshStates[0].texture=new C8(this.gl,m);else{const v=new DN(this.gl,this.mainProgram.attrs.a_pos,this.mainProgram.attrs.a_color,this.mainProgram.attrs.a_uv);v.resetSubdivition(50);const x=Math.random()>.8?_N(6,6):b8[Math.floor(Math.random()*b8.length)];v.resizeControlPoints(x.width,x.height);const b=2/(x.width-1),C=2/(x.height-1);for(const S of x.conf){const L=v.getControlPoint(S.cx,S.cy);L.location.x=S.x,L.location.y=S.y,L.uRot=S.ur*Math.PI/180,L.vRot=S.vr*Math.PI/180,L.uScale=b*S.up,L.vScale=C*S.vp}v.updateMesh();const T={mesh:v,texture:new C8(this.gl,m),alpha:0};this.meshStates.push(T)}this.requestTick()}setLowFreqVolume(n){this.volume=n/10}setHasLyric(n){}dispose(){super.dispose(),this.tickHandle&&(cancelAnimationFrame(this.tickHandle),this.tickHandle=0),this._disposed=!0,this.mainProgram.dispose(),this.quadProgram.dispose(),this.gl.deleteBuffer(this.quadBuffer),this.fbo&&this.gl.deleteFramebuffer(this.fbo),this.fboTexture&&this.gl.deleteTexture(this.fboTexture);for(const n of this.meshStates)n.mesh.dispose(),n.texture.dispose()}enablePerformanceMonitor(n){this.enablePerformanceMonitoring=n,n&&(this.frameCount=0,this.lastFPSUpdate=performance.now())}getCurrentFPS(){return this.currentFPS}updatePerformanceStats(n){this.enablePerformanceMonitoring&&(this.frameCount++,n-this.lastFPSUpdate>1e3&&(this.currentFPS=this.frameCount,this.frameCount=0,this.lastFPSUpdate=n))}},PN=class extends Ir{time=0},ib=class extends tb{canvas;app;curContainer;staticMode=!1;lastContainer=new Set;onTick=n=>{for(const t of this.lastContainer)t.alpha=ns(t.alpha-n/60),t.alpha<=0&&(this.app.stage.removeChild(t),this.lastContainer.delete(t),t.destroy(!0));if(this.curContainer){this.curContainer.alpha=Math.min(1,this.curContainer.alpha+n/60);const[t,e,i,a]=this.curContainer.children,o=Math.max(this.app.screen.width,this.app.screen.height);t.position.set(this.app.screen.width/2,this.app.screen.height/2),e.position.set(this.app.screen.width/2.5,this.app.screen.height/2.5),i.position.set(this.app.screen.width/2,this.app.screen.height/2),a.position.set(this.app.screen.width/2,this.app.screen.height/2),t.width=o*Math.sqrt(2),t.height=t.width,e.width=o*.8,e.height=e.width,i.width=o*.5,i.height=i.width,a.width=o*.25,a.height=a.width,this.curContainer.time+=n*this.flowSpeed,t.rotation+=n/1e3*this.flowSpeed,e.rotation-=n/500*this.flowSpeed,i.rotation+=n/1e3*this.flowSpeed,a.rotation-=n/750*this.flowSpeed,i.x=this.app.screen.width/2+this.app.screen.width/4*Math.cos(this.curContainer.time/1e3*.75),i.y=this.app.screen.height/2+this.app.screen.width/4*Math.cos(this.curContainer.time/1e3*.75),a.x=this.app.screen.width/2+this.app.screen.width/4*.1+Math.cos(this.curContainer.time*.006*.75),a.y=this.app.screen.height/2+this.app.screen.width/4*.1+Math.cos(this.curContainer.time*.006*.75),this.curContainer.alpha>=1&&this.lastContainer.size===0&&this.staticMode&&this.app.ticker.stop()}};constructor(n){super(n),this.canvas=n,this.app=new Xv({view:n,resizeTo:this.canvas,powerPreference:"low-power",backgroundAlpha:1}),this.rebuildFilters(),this.app.ticker.maxFPS=30,this.app.ticker.add(this.onTick),this.app.ticker.start()}onResize(n,t){super.onResize(n,t),this.app.resize(),this.rebuildFilters()}setRenderScale(n){super.setRenderScale(n),this.rebuildFilters()}rebuildFilters(){const n=Math.min(this.canvas.width,this.canvas.height),t=Math.max(this.canvas.width,this.canvas.height),e=new wl;e.saturate(1.2,!1);const i=new wl;i.brightness(.6,!1);const a=new wl;a.contrast(.3,!0);for(const o of this.app.stage.filters??[])o.destroy();this.app.stage.filters=[],this.app.stage.filters.push(new Is(5,1)),this.app.stage.filters.push(new Is(10,1)),this.app.stage.filters.push(new Is(20,2)),this.app.stage.filters.push(new Is(40,2)),this.app.stage.filters.push(new Is(80,2)),n>768&&this.app.stage.filters.push(new Is(160,4)),n>768*2&&this.app.stage.filters.push(new Is(320,4)),this.app.stage.filters.push(e,i,a),this.app.stage.filters.push(new Is(5,1)),Math.random()>.5?(this.app.stage.filters.push(new vl({radius:(t+n)/2,strength:1,center:[.25,1]})),this.app.stage.filters.push(new vl({radius:(t+n)/2,strength:1,center:[.75,0]}))):(this.app.stage.filters.push(new vl({radius:(t+n)/2,strength:1,center:[.75,1]})),this.app.stage.filters.push(new vl({radius:(t+n)/2,strength:1,center:[.25,0]})))}setStaticMode(n=!1){this.staticMode=n,this.app.ticker.start()}setFPS(n){this.app.ticker.maxFPS=n}pause(){this.app.ticker.stop(),this.app.render()}resume(){this.app.ticker.start()}setLowFreqVolume(n){}setHasLyric(n){}async setAlbum(n,t){if(!n||typeof n=="string"&&n.trim().length===0)return;let e=null,i=5,a=null;for(;!a?.baseTexture?.resource?.valid&&i>0;)try{typeof n=="string"?e=await Bm(n,t):e=await eb(n),a=ne.from(e,{resourceOptions:{autoLoad:!1}}),await a.baseTexture.resource.load()}catch(m){console.warn(`failed on loading album image, retrying (${i})`,n,m),a=null,i--}if(!a)return;const o=new PN,c=new Va(a),u=new Va(a),f=new Va(a),d=new Va(a);c.anchor.set(.5,.5),u.anchor.set(.5,.5),f.anchor.set(.5,.5),d.anchor.set(.5,.5),c.rotation=Math.random()*Math.PI*2,u.rotation=Math.random()*Math.PI*2,f.rotation=Math.random()*Math.PI*2,d.rotation=Math.random()*Math.PI*2,o.addChild(c,u,f,d),this.curContainer&&this.lastContainer.add(this.curContainer),this.curContainer=o,this.app.stage.addChild(o),this.curContainer.alpha=0,this.app.ticker.start()}dispose(){super.dispose(),this.app.ticker.remove(this.onTick),this.app.destroy(!0)}getElement(){return this.canvas}},FN=class sb{element;renderer;constructor(t,e){this.renderer=t,this.element=e,e.style.pointerEvents="none",e.style.zIndex="-1",e.style.contain="strict"}static new(t){const e=document.createElement("canvas");return new sb(new t(e),e)}setRenderScale(t){this.renderer.setRenderScale(t)}setFlowSpeed(t){this.renderer.setFlowSpeed(t)}setStaticMode(t){this.renderer.setStaticMode(t)}setFPS(t){this.renderer.setFPS(t)}pause(){this.renderer.pause()}resume(){this.renderer.resume()}setLowFreqVolume(t){this.renderer.setLowFreqVolume(t)}setHasLyric(t){this.renderer.setHasLyric(t)}setAlbum(t,e){return this.renderer.setAlbum(t,e)}getElement(){return this.element}dispose(){this.renderer.dispose(),this.element.remove()}},Ut={active:"FmKaba_active",bgWrapper:"FmKaba_bgWrapper",bgWrapperActive:"FmKaba_bgWrapperActive",bgWrapperHidden:"FmKaba_bgWrapperHidden",bgWrapperTop:"FmKaba_bgWrapperTop",bottomLine:"FmKaba_bottomLine",disableSpring:"FmKaba_disableSpring",emphasize:"FmKaba_emphasize",emphasizeWrapper:"FmKaba_emphasizeWrapper",enabled:"FmKaba_enabled",hasDuetLine:"FmKaba_hasDuetLine",interludeDots:"FmKaba_interludeDots",lyricBgLine:"FmKaba_lyricBgLine",lyricDuetLine:"FmKaba_lyricDuetLine",lyricLine:"FmKaba_lyricLine",lyricLineWrapper:"FmKaba_lyricLineWrapper",lyricMainLine:"FmKaba_lyricMainLine",lyricSubLine:"FmKaba_lyricSubLine",playing:"FmKaba_playing",romanWord:"FmKaba_romanWord",rubyWord:"FmKaba_rubyWord",tmpDisableTransition:"FmKaba_tmpDisableTransition",wordBody:"FmKaba_wordBody",wordWithRuby:"FmKaba_wordWithRuby"};const UN={normalizeSpaces:!0,resetLineTimestamps:!0,convertExcessiveBackgroundLines:!0,syncMainAndBackgroundLines:!0,cleanUnintentionalOverlaps:!0,tryAdvanceStartTime:!0};function kN(n){for(const t of n)for(const e of t.words)e.word=e.word.replace(/\s+/g," ")}function zN(n){for(const t of n)if(t.words.length===1&&t.words[0].startTime===0&&t.words[0].endTime===0&&(t.startTime!==0||t.endTime!==0))t.words[0].startTime=t.startTime,t.words[0].endTime=t.endTime;else if(t.words.length>0){const e=t.words[0],i=t.words[t.words.length-1];t.startTime=e.startTime,t.endTime=i.endTime}}function VN(n){let t=0;for(const e of n)e.isBG?(t++,t>1&&(e.isBG=!1)):t=0}function GN(n){for(let t=n.length-1;t>=0;t--){const e=n[t];if(e.isBG)continue;const i=n[t+1];if(i?.isBG){const a=[...e.words,...i.words].filter(o=>o.word.trim().length>0);if(a.length>0){const o=Math.min(...a.map(d=>d.startTime)),c=Math.max(...a.map(d=>d.endTime)),u=Math.min(o,e.startTime,i.startTime),f=Math.max(c,e.endTime,i.endTime);e.startTime=u,e.endTime=f,i.startTime=u,i.endTime=f}}}}function jN(n){for(let t=0;t0){const c=(a.endTime-a.startTime)*.1;if(!(o>100&&o>c)){e.endTime=a.startTime;const u=n[t+1];u?.isBG&&(u.endTime=a.startTime)}}}}}function HN(n){let a=0,o=0,c=0,u=0,f=!1;for(let d=0;d=o)x=600,b=u;else{x=400;const L=o-a;b=a+L*.3}else x=600,b=0;const C=m.startTime-x,T=Math.max(b,C);Tc?(c=Math.min(c,g),u=Math.max(u,v)):(c=g,u=v),a=g,o=v,f=!0}}function qN(n,t){const e={...UN,...t};e.normalizeSpaces&&kN(n),e.resetLineTimestamps&&zN(n),e.convertExcessiveBackgroundLines&&VN(n),e.syncMainAndBackgroundLines&&GN(n),e.cleanUnintentionalOverlaps&&jN(n),e.tryAdvanceStartTime&&HN(n)}function XN(n){const t=2.5949095;return n<.5?(2*n)**2*((t+1)*2*n-t)/2:((2*n-2)**2*((t+1)*(n*2-2)+t)+2)/2}function YN(n){return n===1?1:1-2**(-10*n)}var $N=class{element=document.createElement("div");dot0=document.createElement("span");dot1=document.createElement("span");dot2=document.createElement("span");left=0;top=0;playing=!0;lastStyle="";currentInterlude;currentTime=0;targetBreatheDuration=1500;constructor(){this.element.className=Ut.interludeDots,this.element.appendChild(this.dot0),this.element.appendChild(this.dot1),this.element.appendChild(this.dot2)}getElement(){return this.element}setTransform(n=this.left,t=this.top){this.left=n,this.top=t,this.update()}setInterlude(n){this.currentInterlude=n,this.currentTime=n?.[0]??0,n?this.element.classList.add(Ut.enabled):this.element.classList.remove(Ut.enabled)}pause(){this.playing=!1,this.element.classList.remove(Ut.playing)}resume(){this.playing=!0,this.element.classList.add(Ut.playing)}update(n=0){if(!this.playing)return;this.currentTime+=n;let t="";if(t+=`transform:translate(${this.left.toFixed(2)}px, ${this.top.toFixed(2)}px)`,this.currentInterlude){const e=this.currentInterlude[1]-this.currentInterlude[0],i=this.currentTime-this.currentInterlude[0];if(i<=e){const a=e/Math.ceil(e/this.targetBreatheDuration);let o=1,c=1;o*=Math.sin(1.5*Math.PI-i/a*2)/20+1,i<2e3&&(o*=YN(i/2e3)),i<500?c=0:i<1e3&&(c*=(i-500)/500),e-i<750&&(o*=1-XN((750-(e-i))/750/2)),e-i<375&&(c*=li((e-i)/375));const u=ns(e-750);o=ns(o)*.7,t+=` scale(${o})`;const f=ks(.25,i*3/u*.75,1),d=ks(.25,(i-u/3)*3/u*.75,1),m=ks(.25,(i-u/3*2)*3/u*.75,1);this.dot0.style.opacity=`${li(c*f)}`,this.dot1.style.opacity=`${li(c*d)}`,this.dot2.style.opacity=`${li(c*m)}`}else t+=" scale(0)",this.dot0.style.opacity="0",this.dot1.style.opacity="0",this.dot2.style.opacity="0";t+=";",this.lastStyle!==t&&(this.element.setAttribute("style",t),this.lastStyle=t)}}dispose(){this.element.remove()}};const Pm=[],S8=[];let Fm=!1;function WN(){let n=S8.shift();for(;n;){try{n.resolve(n.task())}catch(t){n.reject(t)}n=S8.shift()}for(n=Pm.shift();n;){try{n.resolve(n.task())}catch(t){n.reject(t)}n=Pm.shift()}Fm=!1}function ZN(){Fm||(Fm=!0,requestAnimationFrame(WN))}function KN(n){const t={task:n,resolve:()=>{},reject:()=>{}},e=new Promise((i,a)=>{t.resolve=i,t.reject=a});return Pm.push(t),ZN(),e}function QN(n){return e=>(n(e+.001)-n(e-.001))/(2*.001)}function _8(n){return QN(n)}var zl=class{currentPosition=0;targetPosition=0;currentTime=0;params={};currentSolver;getV;getV2;queueParams;queuePosition;constructor(n=0){this.targetPosition=n,this.currentPosition=this.targetPosition,this.currentSolver=()=>this.targetPosition,this.getV=()=>0,this.getV2=()=>0}resetSolver(){const n=this.getV(this.currentTime);this.currentTime=0,this.currentSolver=JN(this.currentPosition,n,this.targetPosition,0,this.params),this.getV=_8(this.currentSolver),this.getV2=_8(this.getV)}arrived(){return Math.abs(this.targetPosition-this.currentPosition)<.01&&this.getV(this.currentTime)<.01&&this.getV2(this.currentTime)<.01&&this.queueParams===void 0&&this.queuePosition===void 0}setPosition(n){this.targetPosition=n,this.currentPosition=n,this.currentSolver=()=>this.targetPosition,this.getV=()=>0,this.getV2=()=>0}update(n=0){this.currentTime+=n,this.currentPosition=this.currentSolver(this.currentTime),this.queueParams&&(this.queueParams.time-=n,this.queueParams.time<=0&&this.updateParams({...this.queueParams})),this.queuePosition&&(this.queuePosition.time-=n,this.queuePosition.time<=0&&this.setTargetPosition(this.queuePosition.position)),this.arrived()&&this.setPosition(this.targetPosition)}updateParams(n,t=0){t>0?this.queueParams={...this.queuePosition??{},...n,time:t}:(this.queuePosition=void 0,this.params={...this.params,...n},this.resetSolver())}setTargetPosition(n,t=0){t>0?this.queuePosition={...this.queuePosition??{},position:n,time:t}:(this.queuePosition=void 0,this.targetPosition=n,this.resetSolver())}getCurrentPosition(){return this.currentPosition}};function JN(n,t,e,i=0,a){const o=a?.soft??!1,c=a?.stiffness??100,u=a?.damping??10,f=a?.mass??1,d=e-n;if(o||1<=u/(2*Math.sqrt(c*f))){const b=-Math.sqrt(c/f),C=-b*d-t;return T=>(T-=i,T<0?n:e-(d+T*C)*Math.E**(T*b))}const m=Math.sqrt(4*f*c-u**2),g=(u*d-2*f*t)/m,v=.5*m/f,x=-(.5*u)/f;return b=>(b-=i,b<0?n:e-(Math.cos(b*v)*d+Math.sin(b*v)*g)*Math.E**(b*x))}var tO=class{lyricPlayer;element=document.createElement("div");left=0;top=0;delay=0;lineSize=[0,0];lineTransforms={posX:new zl(0),posY:new zl(0)};isFocused=!1;blur=0;constructor(n){this.lyricPlayer=n,this.element.setAttribute("class",`${Ut.lyricLine} ${Ut.bottomLine}`),this.element.dataset.bottomLine="true",this.rebuildStyle()}async measureSize(){return await KN(()=>[this.element.clientWidth,this.element.clientHeight])}lastStyle="";show(){this.rebuildStyle()}hide(){this.rebuildStyle()}setFocused(n){this.isFocused!==n&&(this.isFocused=n,n?this.element.dataset.focused="true":delete this.element.dataset.focused)}rebuildStyle(){let n=`transform:translate(${this.lineTransforms.posX.getCurrentPosition().toFixed(2)}px,${this.lineTransforms.posY.getCurrentPosition().toFixed(2)}px);`;!this.lyricPlayer.getEnableSpring()&&this.isInSight&&(n+=`transition-delay:${this.delay}ms;`),n+=`filter:blur(${Math.min(5,this.blur)}px);`,n!==this.lastStyle&&(this.lastStyle=n,this.element.setAttribute("style",n))}getElement(){return this.element}setTransform(n=this.left,t=this.top,e=0,i=!1,a=0){this.left=n,this.top=t,this.delay=a*1e3|0,i||!this.lyricPlayer.getEnableSpring()?(this.blur=Math.min(32,e),i&&this.element.classList.add(Ut.tmpDisableTransition),this.lineTransforms.posX.setPosition(n),this.lineTransforms.posY.setPosition(t),this.lyricPlayer.getEnableSpring()?this.rebuildStyle():this.show(),i&&requestAnimationFrame(()=>{this.element.classList.remove(Ut.tmpDisableTransition)})):(this.blur=Math.min(5,e),this.lineTransforms.posX.setTargetPosition(n,a),this.lineTransforms.posY.setTargetPosition(t,a))}update(n=0){this.lyricPlayer.getEnableSpring()&&(this.lineTransforms.posX.update(n),this.lineTransforms.posY.update(n),this.isInSight?this.show():this.hide())}get isInSight(){const n=this.lineTransforms.posX.getCurrentPosition(),t=this.lineTransforms.posY.getCurrentPosition(),e=n+this.lineSize[0],i=t+this.lineSize[1],a=this.lyricPlayer.size[0],o=this.lyricPlayer.size[1];return!(n>a||t>o||e<0||i<0)}dispose(){this.element.remove()}};const Ba={Disabled:"",FullMask:"full-mask",PartialMask:"partial-mask"},wr={SOLID:0,GRADIENT:1},D0={Center:"center",Bottom:"bottom"};function eO(n){const t=n.currentTime+20,e=n.scrollToIndex,i=n.currentGroups,a=o=>{if(o<-1||o>=i.length-1)return;const c=o===-1?null:i[o],u=i[o+1],f=c?c.endTime:0,d=Math.max(f,u.startTime-250);if(!(d-f<4e3)&&d>t&&f=e&&t{e.onBeginScroll()&&(t.isUserScrolling=!0,g.preventDefault(),i=t.scrollOffset,a=g.touches[0].screenY,u=a,o=g.touches[0].screenX,c=g.touches[0].screenY,f=Date.now(),d=0,e.onLayout(!0,!0))}),n.addEventListener("touchmove",g=>{if(e.onBeginScroll()){g.preventDefault();const v=g.touches[0].screenY,x=v-a;t.scrollOffset=i-x,Ou(t);const b=Date.now(),C=b-f;C>0&&(d=(v-u)/C),u=v,f=b,e.onLayout(!0,!0)}}),n.addEventListener("touchend",g=>{if(e.onBeginScroll()){g.preventDefault();const v=g.changedTouches[0],x=Math.abs(v.screenX-o),b=Math.abs(v.screenY-c);if(x<10&&b<10){const L=document.elementFromPoint(v.clientX,v.clientY);L instanceof HTMLElement&&e.containsTarget(L)&&e.clickTarget(L),t.isUserScrolling=!1,e.onEndScroll();return}a=0;const C=++m;Math.abs(d)<.1&&(d=0);let T=performance.now();const S=L=>{if(C!==m)return;const R=L-T;if(T=L,R<=0||R>100){requestAnimationFrame(S);return}if(Math.abs(d)>.05){t.scrollOffset-=d*R,Ou(t);const P=.95**(R/16);d*=P,e.onLayout(!0,!0),requestAnimationFrame(S)}else t.isUserScrolling=!1,e.onEndScroll()};requestAnimationFrame(S)}else t.isUserScrolling=!1}),n.addEventListener("wheel",g=>{e.onBeginScroll()&&(g.preventDefault(),g.deltaMode===g.DOM_DELTA_PIXEL?(t.scrollOffset+=g.deltaY,Ou(t),e.onLayout(!0,!1)):(t.scrollOffset+=g.deltaY*50,Ou(t),e.onLayout(!1,!1)))},{passive:!1})}const aO=(n,t)=>n.size===t.size&&[...n].every(e=>t.has(e));function oO(n){const{time:t,currentGroups:e,timelineState:{hotGroups:i,bufferedGroups:a}}=n,o=new Set(i),c=new Set,u=new Set,f=new Set;for(const d of i){const m=e[d];(!m||tt&&!o.has(d)&&(o.add(d),c.add(d))}for(const d of a)o.has(d)||f.add(d);return{nextHotGroups:o,addedIds:c,removedHotIds:u,removedBufferedIds:f}}function lO(n,t,e){if(e.size>0)return Math.min(...e);const i=t.findIndex(a=>a.startTime>=n);return i===-1?t.length:i}function cO(n){const{timelineState:t,time:e,currentGroups:i,hasBottomContent:a,stateResult:o}=n,{addedIds:c,removedHotIds:u,removedBufferedIds:f}=o,{isSeeking:d}=t;t.currentTime=e,t.hotGroups=o.nextHotGroups;let m=!1,g=!1;const v=[],x=new Set;if(d){t.bufferedGroups=new Set([...t.hotGroups]),t.scrollToIndex=lO(e,i,t.bufferedGroups);for(const b of u)x.add(b);for(const b of t.hotGroups)v.push(b);for(const b of f)x.add(b);g=!0,m=!0}else if(c.size>0){for(const b of c)t.bufferedGroups.add(b),v.push(b);for(const b of f)t.bufferedGroups.delete(b),x.add(b);t.bufferedGroups.size>0&&(t.scrollToIndex=Math.min(...t.bufferedGroups)),m=!0}else if(f.size>0&&aO(f,t.bufferedGroups)){for(const b of t.bufferedGroups)t.hotGroups.has(b)||(t.bufferedGroups.delete(b),x.add(b));m=!0}if(t.bufferedGroups.size===0&&i.length>0&&e>=i[i.length-1].endTime){const b=a?i.length:i.length-1;t.scrollToIndex!==b&&(t.scrollToIndex=b,m=!0)}return t.lastCurrentTime=e,{shouldLayout:m,shouldResetScroll:g,groupsToEnable:v,groupsToDisable:[...x]}}var uO=class extends EventTarget{element=document.createElement("div");timelineState={currentTime:0,lastCurrentTime:0,hotGroups:new Set,bufferedGroups:new Set,scrollToIndex:0,isSeeking:!1,isPlaying:!0,initialLayoutFinished:!1};lyricGroupElementMap=new WeakMap;currentLyricLines=[];processedLines=[];lyricLinesIndexes=new WeakMap;isNonDynamic=!1;hasDuetLine=!1;disableSpring=!1;layoutState={interludeDotsSize:[0,0],targetAlignIndex:0,lastInterludeState:!1,alignAnchor:D0.Center,alignPosition:.35,overscanPx:300};interludeDots=new $N;bottomLine=new tO(this);enableBlur=!0;enableScale=!0;maskObsceneWords=Ba.Disabled;maskObsceneWordChar="*";hidePassedLines=!1;scrollState={scrollBoundary:{minOffset:0,maxOffset:0},scrollOffset:0,allowScroll:!0,isScrolled:!1,isUserScrolling:!1};currentLyricGroups=[];lyricGroupSize=new WeakMap;size=[0,0];isPageVisible=!0;optimizeOptions={};alwaysPostpositionBackground=!1;posXSpringParams={mass:1,damping:10,stiffness:100};posYSpringParams={mass:.9,damping:15,stiffness:90};scaleSpringParams={mass:2,damping:25,stiffness:100};scaleForBGSpringParams={mass:1,damping:20,stiffness:50};onPageShow=()=>{this.isPageVisible=!0,this.setCurrentTime(this.timelineState.currentTime,!0)};onPageHide=()=>{this.isPageVisible=!1};scrolledHandler;resizeObserver=new ResizeObserver((n=>{let t=!1,e=!1;for(const i of n)if(i.target===this.element){const a=i.contentRect;this.size[0]=a.width,this.size[1]=a.height,e=!0}else if(i.target===this.interludeDots.getElement())this.layoutState.interludeDotsSize[0]=i.target.clientWidth,this.layoutState.interludeDotsSize[1]=i.target.clientHeight,t=!0;else if(i.target===this.bottomLine.getElement()){const a=[i.target.clientWidth,i.target.clientHeight],o=this.bottomLine.lineSize;(a[0]!==o[0]||a[1]!==o[1])&&(this.bottomLine.lineSize=a,t=!0)}else{const a=this.lyricGroupElementMap.get(i.target);if(a){const o=[i.target.clientWidth,i.target.clientHeight],c=this.lyricGroupSize.get(a)??[0,0];(o[0]!==c[0]||o[1]!==c[1])&&(this.lyricGroupSize.set(a,o),a.onLineSizeChange(o),t=!0)}}t&&this.calcLayout(!0),e&&this.onResize()}));wordFadeWidth=.5;constructor(n){super(),n&&(this.element=n),this.element.classList.add("amll-lyric-player"),this.resizeObserver.observe(this.element),this.resizeObserver.observe(this.interludeDots.getElement()),this.element.appendChild(this.interludeDots.getElement()),this.element.appendChild(this.bottomLine.getElement()),this.interludeDots.setTransform(0,200),window.addEventListener("pageshow",this.onPageShow),window.addEventListener("pagehide",this.onPageHide),rO(this.element,this.scrollState,{onBeginScroll:()=>this.beginScrollHandler(),onEndScroll:()=>this.endScrollHandler(),onLayout:(t,e)=>this.calcLayout(t,e),containsTarget:t=>this.element.contains(t),clickTarget:t=>t.click()})}beginScrollHandler(){const n=this.scrollState.allowScroll;return n&&(this.scrollState.isScrolled=!0,clearTimeout(this.scrolledHandler),this.scrolledHandler=setTimeout(()=>{this.scrollState.isScrolled=!1,this.scrollState.scrollOffset=0},5e3)),n}endScrollHandler(){}setWordFadeWidth(n=.5){this.wordFadeWidth=Math.max(1e-4,n)}setEnableScale(n=!0){this.enableScale=n,this.calcLayout()}getEnableScale(){return this.enableScale}getWordFadeWidth(){return this.wordFadeWidth}setIsSeeking(n){this.timelineState.isSeeking=n}setHidePassedLines(n){this.hidePassedLines=n,this.calcLayout()}setEnableBlur(n){this.enableBlur!==n&&(this.enableBlur=n,this.calcLayout())}setMaskObsceneWords(n){this.maskObsceneWords!==n&&(this.maskObsceneWords=n,this.rebuildLyricLines(),this.calcLayout())}setMaskObsceneWordChar(n){const t=n.charAt(0)||"*";this.maskObsceneWordChar!==t&&(this.maskObsceneWordChar=t,this.maskObsceneWords!==Ba.Disabled&&(this.rebuildLyricLines(),this.calcLayout()))}rebuildLyricLines(){for(const n of this.currentLyricGroups)n.rebuildAllLines()}processObsceneWord(n){const t=n.word;if(!n.obscene||this.maskObsceneWords===Ba.Disabled)return t;const e=this.maskObsceneWordChar;if(this.maskObsceneWords===Ba.FullMask)return t.replace(/\S/g,e);if(this.maskObsceneWords===Ba.PartialMask){const i=t.trim();if(i.length<=2)return t.replace(/\S/g,e);const a=t.indexOf(i),o=a+i.length-1;return t.slice(0,a+1)+t.slice(a+1,o).replace(/\S/g,e)+t.slice(o)}return t}setAlignAnchor(n){this.layoutState.alignAnchor=n}setAlignPosition(n){this.layoutState.alignPosition=n}setOverscanPx(n){this.layoutState.overscanPx=ns(n|0)}getOverscanPx(){return this.layoutState.overscanPx}setEnableSpring(n=!0){this.disableSpring=!n,n?this.element.classList.remove(Ut.disableSpring):this.element.classList.add(Ut.disableSpring),this.calcLayout(!0)}getEnableSpring(){return!this.disableSpring}setOptimizeOptions(n){this.optimizeOptions={...this.optimizeOptions,...n}}setLyricLines(n,t=0){this.timelineState.initialLayoutFinished=!0,this.timelineState.lastCurrentTime=t,this.timelineState.currentTime=t,this.currentLyricLines=Dm(n),this.processedLines=Dm(this.currentLyricLines),qN(this.processedLines,this.optimizeOptions),this.isNonDynamic=!0;for(const e of this.processedLines)if(e.words.length>1){this.isNonDynamic=!1;break}this.hasDuetLine=this.processedLines.some(e=>e.isDuet);for(const e of this.currentLyricGroups)e.dispose();this.currentLyricGroups=[],this.interludeDots.setInterlude(void 0),this.timelineState.hotGroups.clear(),this.timelineState.bufferedGroups.clear()}getIsPlaying(){return this.timelineState.isPlaying}setCurrentTime(n,t=!1){n=Math.round(n);const{timelineState:e}=this;if(e.isSeeking=!!t,e.currentTime=n,!e.initialLayoutFinished&&!e.isSeeking)return;const i=oO({time:n,currentGroups:this.currentLyricGroups,timelineState:e}),a=this.bottomLine.getElement().innerHTML.trim().length>0,o=cO({timelineState:e,time:n,currentGroups:this.currentLyricGroups,hasBottomContent:a,stateResult:i});for(const c of o.groupsToDisable)this.currentLyricGroups[c]?.disable();for(const c of o.groupsToEnable)this.currentLyricGroups[c]?.enable();o.shouldResetScroll&&this.resetScroll(),o.shouldLayout&&this.calcLayout()}async calcLayout(n=!1,t=!1){const e=eO({currentTime:this.timelineState.currentTime,scrollToIndex:this.timelineState.scrollToIndex,currentGroups:this.currentLyricGroups}),i=!!e;if(this.layoutState.targetAlignIndex!==this.timelineState.scrollToIndex||this.layoutState.lastInterludeState!==i){this.layoutState.lastInterludeState=i;const P=nO({enabled:this.getEnableSpring(),currentGroups:this.currentLyricGroups,scrollToIndex:this.timelineState.scrollToIndex,isSeeking:this.timelineState.isSeeking,isInterludeActive:i});P.shouldUpdate&&P.params&&this.setLinePosYSpringParams(P.params)}let a=-this.scrollState.scrollOffset;const o=this.timelineState.scrollToIndex;let c=!1;e?c=e.isNextDuet:this.interludeDots.setInterlude(void 0);const u=(this.baseFontSize||24)*.4,f=this.layoutState.interludeDotsSize[1]+u*2;e&&e.anchorLineIndex!==-1&&(a-=f);const d=this.size[1]/5,m=this.currentLyricGroups.slice(0,o).reduce((P,w)=>P+(this.lyricGroupSize.get(w)?.[1]??d),0);this.scrollState.scrollBoundary.minOffset=-m,a-=m,a+=this.size[1]*this.layoutState.alignPosition;const g=this.currentLyricGroups[o];this.layoutState.targetAlignIndex=o;const v=o===this.currentLyricGroups.length;this.bottomLine.setFocused(v);const x=g?this.lyricGroupSize.get(g)?.[1]??d:v?this.bottomLine.lineSize[1]:0;if(x>0)switch(this.layoutState.alignAnchor){case D0.Bottom:a-=x;break;case D0.Center:a-=x/2;break}const b=Math.max(...this.timelineState.bufferedGroups);let C=0,T=n?0:.05,S=!1;this.currentLyricGroups.forEach((P,w)=>{const F=this.timelineState.bufferedGroups.has(w),E=e&&w===e.anchorLineIndex+1;if(!S&&E){S=!0,a+=u;let I=0;e&&c&&(I=this.size[0]-this.layoutState.interludeDotsSize[0]),this.interludeDots.setTransform(I,a),e&&this.interludeDots.setInterlude([e.startTime,e.endTime]),a+=this.layoutState.interludeDotsSize[1],a+=u}const O=iO({groupIndex:w,scrollToIndex:this.timelineState.scrollToIndex,latestIndex:b,hasBuffered:F,hidePassedLines:this.hidePassedLines,isPlaying:this.timelineState.isPlaying,isNonDynamic:this.isNonDynamic,enableBlur:this.enableBlur,isUserScrolling:this.scrollState.isUserScrolling,isCompact:window.innerWidth<=1024,interlude:e});P.setTransform(a,t,C,O.isActive,O.targetOpacity,O.blurLevel),a+=this.lyricGroupSize.get(P)?.[1]??d,a>=0&&!this.timelineState.isSeeking&&(C+=T,w>=this.timelineState.scrollToIndex&&(T/=1.05))}),this.scrollState.scrollBoundary.maxOffset=a+this.scrollState.scrollOffset-this.size[1]/2;const L=this.currentLyricGroups.length,R=rb({enableBlur:this.enableBlur,isUserScrolling:this.scrollState.isUserScrolling,isActive:v,itemIndex:L,scrollToIndex:this.timelineState.scrollToIndex,latestIndex:b,isCompact:window.innerWidth<=1024});this.bottomLine.setTransform(0,a,R,t,C)}setLinePosXSpringParams(n={}){}setLinePosYSpringParams(n={}){this.posYSpringParams={...this.posYSpringParams,...n},this.bottomLine.lineTransforms.posY.updateParams(this.posYSpringParams);for(const t of this.currentLyricGroups)t.posY.updateParams(this.posYSpringParams),t.bgSlideY.updateParams(this.posYSpringParams)}setLineScaleSpringParams(n={}){this.scaleSpringParams={...this.scaleSpringParams,...n},this.scaleForBGSpringParams={...this.scaleForBGSpringParams,...n};for(const t of this.currentLyricGroups)t.mainLine.lineTransforms.scale.updateParams(this.scaleSpringParams),t.bgLine?.lineTransforms.scale.updateParams(this.scaleForBGSpringParams)}pause(){this.interludeDots.pause(),this.timelineState.isPlaying&&(this.timelineState.isPlaying=!1,this.calcLayout())}resume(){this.interludeDots.resume(),this.timelineState.isPlaying||(this.timelineState.isPlaying=!0,this.calcLayout())}update(n=0){this.bottomLine.update(n/1e3),this.interludeDots.update(n)}onResize(){}getBottomLineElement(){return this.bottomLine.getElement()}resetScroll(){sO(this.scrollState),clearTimeout(this.scrolledHandler)}getLyricLines(){return this.currentLyricLines}getCurrentTime(){return this.timelineState.currentTime}setAlwaysPostpositionBackground(n){this.alwaysPostpositionBackground!==n&&(this.alwaysPostpositionBackground=n,this.rebuildLyricLines(),this.calcLayout())}getAlwaysPostpositionBackground(){return this.alwaysPostpositionBackground}getElement(){return this.element}dispose(){this.element.remove(),window.removeEventListener("pageshow",this.onPageShow),window.removeEventListener("pagehide",this.onPageHide)}},hO=class{mainLine;bgLine;posY=new zl(0);bgSlideY=new zl(-80);top=0;delay=0;isActive=!1;opacity=1;blur=0;isBgFirst=!1;constructor(n,t){this.mainLine=n,this.bgLine=t}get startTime(){return this.mainLine.getLine().startTime}get endTime(){return this.mainLine.getLine().endTime}onLineSizeChange(n){this.mainLine.onLineSizeChange(n),this.bgLine?.onLineSizeChange(n)}setTransform(n,t,e,i,a,o){this.top=n,this.delay=e,this.isActive=i,this.opacity=a,this.blur=o,this.setLineTransformations(t,e);const c=this.lyricPlayer.getEnableSpring(),u=!this.lyricPlayer.getAlwaysPostpositionBackground()&&this.isBgFirst?80:-80,f=this.lyricPlayer.getIsPlaying(),d=i||!f?0:u;t||!c?(this.posY.setPosition(n),this.bgSlideY.setPosition(d),this.renderStyles()):(this.posY.setTargetPosition(n,e),this.bgSlideY.setTargetPosition(d,e))}setLineTransformations(n,t){const e=this.lyricPlayer.getEnableScale(),i=this.lyricPlayer.getIsPlaying(),a=this.isActive?wr.GRADIENT:wr.SOLID,o=e?97:100;let c=100;!this.isActive&&i&&(c=o),this.mainLine.setTransform(c,1,0,n,t,a);let u=100;!this.isActive&&i&&(u=75),this.bgLine?.setTransform(u,1,0,n,t,a)}update(n){this.lyricPlayer.getEnableSpring()&&(this.posY.update(n),this.bgSlideY.update(n),this.renderStyles()),this.mainLine.update(n),this.bgLine?.update(n)}rebuildAllLines(){this.mainLine.rebuildElement(),this.bgLine?.rebuildElement()}enable(n,t){this.mainLine.enable(n,t),this.bgLine?.enable(n,t)}disable(){this.mainLine.disable(),this.bgLine?.disable()}dispose(){this.mainLine.dispose(),this.bgLine?.dispose()}},fO=class extends hO{lyricPlayer;element;bgWrapper;lastIsActive;constructor(n,t){super(t),this.lyricPlayer=n,this.element=document.createElement("div"),this.element.className=Ut.lyricLineWrapper,this.element.appendChild(t.getElement()),this.posY.setPosition(window.innerHeight*2),n.resizeObserver.observe(this.element)}get isInSight(){const n=this.posY.getCurrentPosition();let t=this.lyricPlayer.lyricGroupSize?.get(this)?.[1];(t===void 0||t===0)&&(t=this.element.clientHeight||0);const e=this.lyricPlayer.size[1],i=this.lyricPlayer.getOverscanPx();return!(n>e+t+i||n<-t-i)}show(){if(!this.element.parentElement){const n=this.lyricPlayer.getElement(),t=this.lyricPlayer.currentLyricGroups,e=t.indexOf(this);let i=null;if(e!==-1){for(let a=e+1;a/^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(n);var Ds=class extends EventTarget{top=0;scale=1;blur=0;opacity=1;delay=0;lineTransforms={scale:new zl(100)};static wordSegmenter=typeof Intl<"u"&&Intl.Segmenter?new Intl.Segmenter(void 0,{granularity:"word"}):null;static graphemeSegmenter=typeof Intl<"u"&&Intl.Segmenter?new Intl.Segmenter(void 0,{granularity:"grapheme"}):null;setTransform(n=this.scale,t=this.opacity,e=this.blur,i=!1,a=0,o=wr.SOLID){this.scale=n,this.opacity=t,this.blur=e,this.delay=a}rebuildElement(){}static shouldEmphasize(n){return Vl(n.word)?n.endTime-n.startTime>=1e3:n.endTime-n.startTime>=1e3&&n.word.trim().length<=7&&n.word.trim().length>1}dispose(){}};const dO=1e3,mO=.15,pO=.5,gO=.4,yO=.6,vO=/[,.;:!?,。;:!?、)】》」』’”)[\]}>~…]$/;function bO(n,t,e,i){const a=n.length;if(a===0||t<=0)return[];const o=new Set;let c=0;for(const{segment:C,isWordLike:T}of i.segment(e))c>0&&T&&[...C].some(S=>Vl(S))&&o.add(c),c+=C.length;const u=new Int32Array(a+1),f=new Float64Array(a+1);for(let C=0;C=0;C--)for(let T=C+1;T<=a;T++){const S=f[T]-f[C];let L=0;if(S>t)if(T===C+1)L=(S-t)**2*dO;else continue;else L=(t-S)**2;let R=0;if(T0&&b0)return;t.resetDOM();const a=this.mainElement.style.whiteSpace;this.mainElement.style.whiteSpace="nowrap";const o=this.mainElement.parentElement;let c="",u=!1;o&&(c=o.style.transform,c&&c!=="none"&&(o.style.transform="none",u=!0));let f=!1;try{const{childInfos:d,fullText:m}=t.buildChildInfos();let g=d.reduce((b,C)=>b+C.width,0);if(t.needsCalibration){const b=document.createRange();b.selectNodeContents(this.mainElement);const C=b.getBoundingClientRect().width;if(g>0&&C>0){const T=C/g;for(const S of d)S.width*=T}g=C}const v=Math.max(1,n);if(g<=v){this.lastBalancedContainerWidth=n;return}const x=bO(d,v,m,e);if(x.length===0){this.lastBalancedContainerWidth=n;return}this.isBalancing=!0,f=!0,t.applyBreaks(x,d),this.lastBalancedContainerWidth=n,this.isBalancing=!1}finally{this.mainElement.style.whiteSpace=a,u&&o&&(o.style.transform=c),f&&(this.isBalancing=!1)}}balanceDynamicLineBreaks(n,t){const e=[];this.executeLineBalance(n,{resetDOM:()=>{this.mainElement.querySelectorAll("br").forEach(i=>{i.remove()})},buildChildInfos:()=>{e.length=0;const i=Array.from(this.mainElement.childNodes),a=[],o=document.createRange();for(const c of i)if(c.nodeType===Node.TEXT_NODE){const u=c.textContent??"";if(u.length===0)continue;o.selectNodeContents(c),a.push({width:o.getBoundingClientRect().width,text:u,isSpace:u.trim().length===0}),e.push(c)}else if(c.nodeType===Node.ELEMENT_NODE){const u=c,f=u.getBoundingClientRect(),d=getComputedStyle(u),m=Number.parseFloat(d.marginLeft)||0,g=Number.parseFloat(d.marginRight)||0;a.push({width:ns(f.width+m+g),text:u.textContent??"",isSpace:!1}),e.push(c)}return{childInfos:a,fullText:a.map(c=>c.text).join("")}},applyBreaks:i=>{for(let a=i.length-1;a>=0;a--){const o=i[a];o>=0&&o{this.mainElement.innerHTML="",this.mainElement.textContent=i},buildChildInfos:()=>{const a=xO();if(!a)return console.debug("Canvas 2D context is not supported, skipping line balancing"),{childInfos:[],fullText:i};a.font=`${t.fontWeight} ${t.fontSize} ${t.fontFamily}`,"letterSpacing"in a&&(a.letterSpacing=t.letterSpacing!=="normal"?t.letterSpacing:"0px"),"wordSpacing"in a&&(a.wordSpacing=t.wordSpacing!=="normal"?t.wordSpacing:"0px");const o=[];for(const{segment:c}of e.segment(i))o.push({width:a.measureText(c).width,text:c,isSpace:c.trim().length===0});return{childInfos:o,fullText:i}},applyBreaks:(a,o)=>{this.mainElement.innerHTML="";const c=new Set(a),u=document.createDocumentFragment();for(let f=0;f{e.length>0&&(t.push(e.length===1?e[0]:[...e]),e=[])},a=o=>{const c=o.word.trim().length===0,u=(o.ruby?.length??0)>0,f=Vl(o.word);!c&&!u&&!f?e.push(o):(i(),t.push(o))};for(const o of n){const c=o.word.trim().length===0,u=o.romanWord??"",f=o.obscene??!1,d=(o.ruby?.length??0)>0;if(c||d){a({...o});continue}const m=o.word.split(CO).filter(b=>b.length>0),g=o.word.replace(SO,"").length||1,v=(o.endTime-o.startTime)/g;let x=0;for(const b of m){if(!b.trim()){const C=o.startTime+x*v;a({word:b,romanWord:"",startTime:C,endTime:C,obscene:f});continue}if(Vl(b)&&b.length>1&&u.trim().length===0){const C=b.split("");for(const T of C){const S=o.startTime+x*v;a({word:T,romanWord:"",startTime:S,endTime:S+v,obscene:f}),x+=1}}else{const C=b.length,T=o.startTime+x*v;a({word:b,romanWord:u,startTime:T,endTime:T+C*v,obscene:f}),x+=C}}}return i(),t}function AO(){return[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]}function EO(n,t=1,e={x:0,y:0}){const[i,a]=[e.x,e.y];return[n[0]*t,n[1]*t,n[2]*t,n[3],n[4]*t,n[5]*t,n[6]*t,n[7],n[8]*t,n[9]*t,n[10]*t,n[11],n[12]-i*t+i,n[13]-a*t+a,n[14],n[15]]}function wO(n,t=4){const e=(i,a)=>i.toFixed(t);return`matrix3d(${n.map(e).join(", ")})`}const Iu=32,ab=(n,t)=>e=>li((e-n)/(t-n)),B2=.5,MO=ab(0,B2),RO=ab(B2,1),LO=Jv(.2,.4,.58,1),NO=Jv(.3,0,.58,1),OO=n=>t=>t=0&&tthis.lyricPlayer.processObsceneWord(c)).join(""),this.setSubLinesText(t,e);return}const i=_O(this.lyricLine.words),a=this.lyricLine.words.some(c=>(c.ruby?.length??0)>0),o=this.lyricLine.words.some(c=>(c.romanWord?.trim().length??0)>0);n.innerHTML="";for(const c of i)this.buildWord(c,n,a,o);this.setSubLinesText(t,e)}setSubLinesText(n,t){n.textContent=this.lyricLine.translatedLyric,t.textContent=this.lyricLine.romanLyric}getRubyCharCount(n){return(n.ruby??[]).reduce((t,e)=>t+e.word.length,0)}getRubySegments(n){return(n.ruby??[]).filter(t=>(t?.word?.trim().length??0)>0)}createWord(n,t,e,i){const a=document.createElement("span"),o=[],c=n.romanWord?.trim()??"",u=e?document.createElement("div"):a;if(e){const d=document.createElement("div"),m=this.getRubySegments(n);for(const g of m){const v=document.createElement("span");v.textContent=g.word,v.dataset.startTime=String(g.startTime),v.dataset.endTime=String(g.endTime),d.appendChild(v)}d.classList.add(Ut.rubyWord),a.classList.add(Ut.wordWithRuby),u.classList.add(Ut.wordBody),a.appendChild(d),a.appendChild(u)}const f=this.lyricPlayer.processObsceneWord(n);if(t){a.classList.add(Ut.emphasize);const d=f.trim();if(Ds.graphemeSegmenter)for(const{segment:m}of Ds.graphemeSegmenter.segment(d)){const g=document.createElement("span");g.textContent=m,o.push(g),u.appendChild(g)}else for(const m of Array.from(d)){const g=document.createElement("span");g.textContent=m,o.push(g),u.appendChild(g)}}else if(i){const d=document.createElement("div");d.textContent=f.trim(),u.appendChild(d)}else c.length===0&&(u.textContent=f.trim());if(i){const d=document.createElement("div");d.textContent=c.length>0?c:" ",d.classList.add(Ut.romanWord),u.appendChild(d)}return{...n,mainElement:a,subElements:o,elementAnimations:[this.initFloatAnimation(n,a)],maskAnimations:[],width:0,height:0,padding:0,shouldEmphasize:t}}buildWord(n,t,e,i){const a=Array.isArray(n)?n:[n];if(a.length===0)return;if(a.every(d=>!d.word.trim())){const d=a.map(m=>m.word).join("");t.appendChild(document.createTextNode(d));return}const o=a.reduce((d,m)=>(d.endTime=Math.max(d.endTime,m.endTime),d.startTime=Math.min(d.startTime,m.startTime),d.word+=m.word,d),{word:"",romanWord:"",startTime:Number.POSITIVE_INFINITY,endTime:Number.NEGATIVE_INFINITY,wordType:"normal",obscene:!1});let c=a.some(d=>Ds.shouldEmphasize(d));Vl(o.word)||(c=c||Ds.shouldEmphasize(o));const u=document.createElement("span");u.classList.add(Ut.emphasizeWrapper);const f=[];for(const d of a){if(!d.word.trim()){u.appendChild(document.createTextNode(d.word));continue}const m=this.createWord(d,c,e,i);c&&f.push(...m.subElements),this.splittedWords.push(m),u.appendChild(m.mainElement)}if(c&&this.splittedWords.length>0){const d=this.splittedWords[this.splittedWords.length-1],m=a.reduce((g,v)=>g+this.getRubyCharCount(v),0);d.elementAnimations.push(...this.initEmphasizeAnimation(o,f,o.endTime-o.startTime,o.startTime-this.lyricLine.startTime,m))}t.appendChild(u)}initFloatAnimation(n,t){const e=n.startTime-this.lyricLine.startTime,i=Math.max(1e3,n.endTime-n.startTime);let a=.05;this.lyricLine.isBG&&(a*=2);const o=t.animate([{transform:"translateY(0px)"},{transform:`translateY(${-a}em)`}],{duration:Number.isFinite(i)?i:0,delay:Number.isFinite(e)?e:0,id:"float-word",composite:"add",fill:"both",easing:"ease-out"});return o.pause(),o}initEmphasizeAnimation(n,t,e,i,a){const o=ns(i);let c=Math.max(1e3,e);const u=a>0?a:Math.max(1,t.length);let f=[],d=c/2e3;d=d>1?Math.sqrt(d):d**3;let m=c/3e3;m=m>1?Math.sqrt(m):m**3,d*=.6,m*=.5,this.lyricLine.words.length>0&&n.word.includes(this.lyricLine.words[this.lyricLine.words.length-1].word)&&(d*=1.6,m*=1.5,c*=1.2),d=Math.min(1.2,d),m=Math.min(.8,m);const g=Number.isFinite(c)?c:0,v=OO(B2);return f=t.flatMap((x,b,C)=>{const T=o+c/2.5/u*b,S=[],L=new Array(Iu).fill(0).map((F,E)=>{const O=(E+1)/Iu,I=v(O),k=v(O)*m,U=EO(AO(),1+I*.1*d),G=-I*.03*d*(C.length/2-b),J=-I*.025*d;return{offset:O,transform:`${wO(U,4)} translate(${G}em, ${J}em)`,textShadow:`0 0 ${Math.min(.3,m*.3)}em rgba(255, 255, 255, ${k})`}}),R=x.animate(L,{duration:g,delay:Number.isFinite(T)?T:0,id:`emphasize-word-${x.textContent}-${b}`,iterations:1,composite:"replace",fill:"both"});R.onfinish=()=>{R.pause()},R.pause(),S.push(R);const P=new Array(Iu).fill(0).map((F,E)=>{const O=(E+1)/Iu;let I=Math.sin(O*Math.PI);return this.lyricLine.isBG&&(I*=2),{offset:O,transform:`translateY(${-I*.05}em)`}}),w=x.animate(P,{duration:g*1.4,delay:Number.isFinite(T)?T-400:0,id:"emphasize-word-float",iterations:1,composite:"add",fill:"both"});return w.onfinish=()=>{w.pause()},w.pause(),S.push(w),S}),f}get totalDuration(){return this.lyricLine.endTime-this.lyricLine.startTime}onLineSizeChange(n){this.updateMaskImageSync()}updateMaskImageSync(){for(const n of this.splittedWords){const t=n.mainElement;t?(n.padding=Number.parseFloat(getComputedStyle(t).paddingLeft),n.width=t.clientWidth-n.padding*2,n.height=t.clientHeight-n.padding*2):(n.width=0,n.height=0,n.padding=0)}if(this.balancer&&Ds.wordSegmenter&&this.balancer.balanceLineBreaks(this.lyricPlayer._getIsNonDynamic(),this.splittedWords.length>0,Ds.wordSegmenter),this.lyricPlayer.supportMaskImage?this.generateWebAnimationBasedMaskImage():this.generateCalcBasedMaskImage(),this.isEnabled){const n=this.lyricPlayer.getIsPlaying?.()??!0;this.enable(this.lyricPlayer.getCurrentTime(),n)}}generateCalcBasedMaskImage(){for(const n of this.splittedWords){const t=n.mainElement;if(t){n.width=t.clientWidth,n.height=t.clientHeight;const e=n.height*this.lyricPlayer.getWordFadeWidth(),[i,a]=A8(e/n.width),o=`${a*100}% 100%`;this.lyricPlayer.supportMaskImage?(t.style.maskImage=i,t.style.maskRepeat="no-repeat",t.style.maskOrigin="left",t.style.maskSize=o):(t.style.webkitMaskImage=i,t.style.webkitMaskRepeat="no-repeat",t.style.webkitMaskOrigin="left",t.style.webkitMaskSize=o);const c=n.width+e,u=`clamp(${-c}px,calc(${-c}px + (var(--amll-player-time) - ${n.startTime})*${c/Math.abs(n.endTime-n.startTime)}px),0px) 0px, left top`;t.style.maskPosition=u,t.style.webkitMaskPosition=u}}}generateWebAnimationBasedMaskImage(){const n=Math.max(0,...this.splittedWords.map(t=>t.endTime),this.lyricLine.endTime)-this.lyricLine.startTime;this.splittedWords.forEach((t,e)=>{const i=t.mainElement;if(i){const a=t.height*this.lyricPlayer.getWordFadeWidth(),[o,c]=A8(a/(t.width+t.padding*2)),u=`${c*100}% 100%`;this.lyricPlayer.supportMaskImage?(i.style.maskImage=o,i.style.maskRepeat="no-repeat",i.style.maskOrigin="left",i.style.maskSize=u):(i.style.webkitMaskImage=o,i.style.webkitMaskRepeat="no-repeat",i.style.webkitMaskOrigin="left",i.style.webkitMaskSize=u);const f=this.splittedWords.slice(0,e).reduce((L,R)=>L+R.width,0)+(this.splittedWords[0]?a:0),d=-(t.width+t.padding*2+a),m=L=>ks(L,d,0);let g=-f-t.width-t.padding-a,v=0;const x=[];let b=g,C=0;const T=()=>{const L=g-b,R=li(v),P=R-C,w=Math.abs(P/L);if(g>d&&b0&&b<0){const E=Math.abs(b)*w,O=`${m(g)}px 0`,I={offset:C+E,maskPosition:O};x.push(I)}const F={offset:R,maskPosition:`${m(g)}px 0`};x.push(F),b=g,C=R};T();let S=0;this.splittedWords.forEach((L,R)=>{{const P=L.startTime-this.lyricLine.startTime,w=P-S;v+=w/n,w>0&&T(),S=P}{const P=ns(L.endTime-L.startTime),w=this.getRubySegments(L),F=w.reduce((E,O)=>E+O.word.length,0);if(F>0){const E=L.width/F;let O=0;for(const U of w){const G=Number.isFinite(U.startTime)?U.startTime:L.startTime,J=Number.isFinite(U.endTime)?U.endTime:L.endTime,tt=Math.max(G,L.startTime),K=Math.min(Math.max(J,tt),L.endTime),A=tt-this.lyricLine.startTime,B=A-S;v+=B/n,B>0&&T(),S=A;const N=ns(K-tt)/U.word.length;for(let H=0;H0&&T(),S+=N,O++}const I=Math.max(L.endTime-this.lyricLine.startTime,S),k=I-S;v+=k/n,k>0&&T(),S=I}else{const O=L.width/1,I=P/1;for(let k=0;k<1;k++)v+=I/n,g+=O,R===0&&k===0&&(g+=a*1.5),R===this.splittedWords.length-1&&k===0&&(g+=a*.5),I>0&&T(),S+=I}}});for(const L of t.maskAnimations)L.cancel();try{const L=i.animate(x,{duration:n||1,id:`fade-word-${t.word}-${e}`,fill:"both"});L.pause(),t.maskAnimations=[L]}catch(L){console.warn("应用渐变动画发生错误",x,n,L)}}})}getElement(){return this.element}updateMaskAlphaTargets(n){const t=li((n-.97)/.03),e=t*.2+.2,i=t*.8+.2;this.renderMode===wr.SOLID?(this.targetBrightAlpha=e,this.targetDarkAlpha=e):(this.targetBrightAlpha=i,this.targetDarkAlpha=e)}applyAlphaToDom(n){const t=n||.016,e=50,i=7,a=u=>1-Math.exp(-u*t),o=a(this.targetBrightAlpha>this.currentBrightAlpha?e:i);Math.abs(this.targetBrightAlpha-this.currentBrightAlpha)<.001?this.currentBrightAlpha=this.targetBrightAlpha:this.currentBrightAlpha+=(this.targetBrightAlpha-this.currentBrightAlpha)*o;const c=a(this.targetDarkAlpha>this.currentDarkAlpha?e:i);Math.abs(this.targetDarkAlpha-this.currentDarkAlpha)<.001?this.currentDarkAlpha=this.targetDarkAlpha:this.currentDarkAlpha+=(this.targetDarkAlpha-this.currentDarkAlpha)*c,this.element.style.setProperty("--bright-mask-alpha",this.currentBrightAlpha.toFixed(3)),this.element.style.setProperty("--dark-mask-alpha",this.currentDarkAlpha.toFixed(3))}setTransform(n=this.scale,t=1,e=0,i=!1,a=0,o=wr.SOLID){super.setTransform(n,t,e,i,a),this.renderMode=o;const c=this.lyricPlayer.getEnableSpring();this.top=0,this.scale=n,this.delay=a*1e3|0;const u=this.element.children[0];if(u.style.opacity=`${t}`,i||!c){this.blur=Math.min(32,e),this.lineTransforms.scale.setPosition(n),this.rebuildStyle();const f=this.lineTransforms.scale.getCurrentPosition();this.updateMaskAlphaTargets(f/100),this.currentBrightAlpha=this.targetBrightAlpha,this.currentDarkAlpha=this.targetDarkAlpha,this.element.style.setProperty("--bright-mask-alpha",String(this.currentBrightAlpha)),this.element.style.setProperty("--dark-mask-alpha",String(this.currentDarkAlpha))}else this.lineTransforms.scale.setTargetPosition(n),this.blur!==Math.min(5,e)&&(this.blur=Math.min(5,e),this.element.style.filter=`blur(${e.toFixed(3)}px)`)}update(n=0){if(!this.lyricPlayer.getEnableSpring()||(this.lineTransforms.scale.update(n),this.rebuildStyle(),!this.built))return;const t=this.lineTransforms.scale.getCurrentPosition()/100;this.updateMaskAlphaTargets(t),this.applyAlphaToDom(n)}_getDebugTargetPos(){return`[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`}teardownContent(){this.built&&(this.disposeElements(),this.built=!1)}disposeElements(){this.balancer?.reset();for(const i of this.splittedWords){for(const a of i.elementAnimations)a.cancel();for(const a of i.maskAnimations)a.cancel();for(const a of i.subElements)a.remove(),a.parentNode?.removeChild(a);i.elementAnimations=[],i.maskAnimations=[],i.subElements=[],i.mainElement?.parentNode&&i.mainElement.parentNode.removeChild(i.mainElement)}this.splittedWords=[];const n=this.element.children[0],t=this.element.children[1],e=this.element.children[2];n&&(n.innerHTML=""),t&&(t.innerHTML=""),e&&(e.innerHTML="")}dispose(){this.disposeElements(),this.lyricPlayer.resizeObserver.unobserve(this.element),this.element.remove()}},DO=class extends MouseEvent{lineIndex;line;bgLine;isPropagationStopped=!1;constructor(n,t,e,i){super(`line-${i.type}`,i),this.lineIndex=n,this.line=t,this.bgLine=e}stopPropagation(){this.isPropagationStopped=!0,super.stopPropagation()}stopImmediatePropagation(){this.isPropagationStopped=!0,super.stopImmediatePropagation()}},ob=class extends uO{abortController=new AbortController;currentLyricGroups=[];onResize(){const n=getComputedStyle(this.element);this._baseFontSize=Number.parseFloat(n.fontSize),this.rebuildStyle()}supportPlusLighter=CSS.supports("mix-blend-mode","plus-lighter");supportMaskImage=CSS.supports("mask-image","none");innerSize=[0,0];onMouseEventHandler=n=>{const t=n.target;if(!(t instanceof Element))return;const e=t.closest(`.${Ut.lyricLineWrapper}`);if(!e)return;const i=this.lyricGroupElementMap.get(e);if(!i)return;const a=i.mainLine,o=i.bgLine,c=new DO(this.lyricLinesIndexes.get(a)??-1,a,o,n);(!this.dispatchEvent(c)||c.defaultPrevented)&&n.preventDefault(),c.isPropagationStopped&&(n.stopPropagation(),n.stopImmediatePropagation())};_getIsNonDynamic(){return this.isNonDynamic}_baseFontSize=Number.parseFloat(getComputedStyle(this.element).fontSize);get baseFontSize(){return this._baseFontSize}constructor(){super(),this.onResize(),this.element.classList.add("amll-lyric-player","dom"),this.disableSpring&&this.element.classList.add(Ut.disableSpring),this.element.addEventListener("click",this.onMouseEventHandler,{signal:this.abortController.signal}),this.element.addEventListener("contextmenu",this.onMouseEventHandler,{signal:this.abortController.signal})}rebuildStyle(){}setWordFadeWidth(n=.5){super.setWordFadeWidth(n);for(const t of this.currentLyricGroups)t.mainLine.updateMaskImageSync(),t.bgLine?.updateMaskImageSync()}setLyricLines(n,t=0){super.setLyricLines(n,t),this.hasDuetLine?this.element.classList.add(Ut.hasDuetLine):this.element.classList.remove(Ut.hasDuetLine),this.supportMaskImage||this.element.style.setProperty("--amll-player-time",`${t}`);for(const i of this.currentLyricGroups)i.dispose();this.currentLyricGroups=[];let e=null;for(let i=0;i{const x=q.useRef(null),b=q.useRef(null),C=q.useRef(null),T=d??Oh;return q.useEffect(()=>{(C.current!==T||x.current===void 0)&&(C.current=T,x.current?.dispose(),x.current=FN.new(T))},[T]),q.useEffect(()=>{T&&n&&x.current?.setAlbum(n,t)},[T,n,t]),q.useEffect(()=>{T&&e&&x.current?.setFPS(e)},[T,e]),q.useEffect(()=>{T&&(i===void 0||i?x.current?.resume():x.current?.pause())},[T,i]),q.useEffect(()=>{T&&a&&x.current?.setFlowSpeed(a)},[T,a]),q.useEffect(()=>{T&&x.current?.setStaticMode(c??!1)},[T,c]),q.useEffect(()=>{T&&o&&x.current?.setRenderScale(o??.5)},[T,o]),q.useEffect(()=>{T&&u&&x.current?.setLowFreqVolume(u??1)},[T,u]),q.useEffect(()=>{T&&f!==void 0&&x.current?.setHasLyric(f??!0)},[T,f]),q.useEffect(()=>{if(x.current){const S=x.current.getElement();S.style.width="100%",S.style.height="100%",S.style.minHeight="0",S.style.minWidth="0",S.style.overflow="hidden",b.current?.appendChild(S)}},[x.current]),q.useImperativeHandle(v,()=>({wrapperEl:b.current,bgRender:x.current}),[b.current,x.current]),Y.jsx("div",{style:{display:"contents",...m},...g,ref:b})}),FO=q.forwardRef(({disabled:n,playing:t,alignAnchor:e,alignPosition:i,enableSpring:a,enableBlur:o,enableScale:c,maskObsceneWordsMode:u,maskObsceneWordChar:f,hidePassedLines:d,optimizeOptions:m,lyricLines:g,currentTime:v,isSeeking:x,wordFadeWidth:b,linePosXSpringParams:C,linePosYSpringParams:T,lineScaleSpringParams:S,bottomLine:L,lyricPlayer:R,onLyricLineClick:P,onLyricLineContextMenu:w,...F},E)=>{const[O,I]=q.useState(),k=q.useRef(null),U=q.useRef(v);return q.useLayoutEffect(()=>{const G=new(R??ob);return I(G),k.current?.appendChild(G.getElement()),()=>{G?.dispose(),I(void 0)}},[R]),q.useLayoutEffect(()=>{m!==void 0&&O?.setOptimizeOptions(m),g!==void 0?(O?.setLyricLines(g,U.current),U.current!==void 0&&O?.setCurrentTime(U.current,!0),O?.update()):(O?.setLyricLines([]),O?.update())},[O,g,m]),q.useEffect(()=>{if(!n){let G=!1,J=-1;const tt=K=>{G||(J===-1&&(J=K),O?.update(K-J),J=K,requestAnimationFrame(tt))};return O?.calcLayout(),requestAnimationFrame(tt),()=>{G=!0}}},[O,n]),q.useEffect(()=>{t!==void 0?t?O?.resume():O?.pause():O?.resume()},[O,t]),q.useEffect(()=>{e!==void 0&&O?.setAlignAnchor(e)},[O,e]),q.useEffect(()=>{d!==void 0&&O?.setHidePassedLines(d)},[O,d]),q.useEffect(()=>{i!==void 0&&O?.setAlignPosition(i)},[O,i]),q.useEffect(()=>{a!==void 0?O?.setEnableSpring(a):O?.setEnableSpring(!0)},[O,a]),q.useEffect(()=>{c!==void 0?O?.setEnableScale(c):O?.setEnableScale(!0)},[O,c]),q.useEffect(()=>{O?.setEnableBlur(o??!0)},[O,o]),q.useLayoutEffect(()=>{v!==void 0?(O?.setCurrentTime(v,x),U.current=v):(O?.setCurrentTime(0),U.current=0)},[O,v,x]),q.useEffect(()=>{O?.setIsSeeking(!!x)},[O,x]),q.useEffect(()=>{O?.setWordFadeWidth(b)},[O,b]),q.useEffect(()=>{C!==void 0&&O?.setLinePosXSpringParams(C)},[O,C]),q.useEffect(()=>{T!==void 0&&O?.setLinePosYSpringParams(T)},[O,T]),q.useEffect(()=>{S!==void 0&&O?.setLineScaleSpringParams(S)},[O,S]),q.useEffect(()=>{u!==void 0?O?.setMaskObsceneWords(u):O?.setMaskObsceneWords(Ba.Disabled)},[O,u]),q.useEffect(()=>{f!==void 0&&O?.setMaskObsceneWordChar(f)},[O,f]),q.useEffect(()=>{if(P){const G=J=>P(J);return O?.addEventListener("line-click",G),()=>O?.removeEventListener("line-click",G)}},[O,P]),q.useEffect(()=>{if(w){const G=J=>w(J);return O?.addEventListener("line-contextmenu",G),()=>O?.removeEventListener("line-contextmenu",G)}},[O,w]),q.useImperativeHandle(E,()=>({wrapperEl:k.current,lyricPlayer:O}),[O]),Y.jsxs(Y.Fragment,{children:[Y.jsx("div",{...F,ref:k}),O?.getBottomLineElement()&&L?BO.createPortal(L,O?.getBottomLineElement()):null]})}),P2={},UO=Symbol((P2?"production":void 0)!=="production"?"RESET":""),lb=n=>typeof n?.then=="function";function kO(n=()=>{try{return window.localStorage}catch(e){(P2?"production":void 0)!=="production"&&typeof window<"u"&&console.warn(e);return}},t){var e;let i,a;const o={getItem:(f,d)=>{var m,g;const v=b=>{if(b=b||"",i!==b){try{a=JSON.parse(b,t?.reviver)}catch{return d}i=b}return a},x=(g=(m=n())==null?void 0:m.getItem(f))!=null?g:null;return lb(x)?x.then(v):v(x)},setItem:(f,d)=>{var m;return(m=n())==null?void 0:m.setItem(f,JSON.stringify(d,void 0))},removeItem:f=>{var d;return(d=n())==null?void 0:d.removeItem(f)}},c=f=>(d,m,g)=>f(d,v=>{let x;try{x=JSON.parse(v||"",t?.reviver)}catch{x=g}m(x)});let u;try{u=(e=n())==null?void 0:e.subscribe}catch{}return!u&&typeof window<"u"&&typeof window.addEventListener=="function"&&window.Storage&&(u=(f,d)=>{if(!(n()instanceof window.Storage))return()=>{};const m=g=>{g.storageArea===n()&&g.key===f&&d(g.newValue)};return window.addEventListener("storage",m),()=>{window.removeEventListener("storage",m)}}),u&&(o.subscribe=c(u)),o}const zO=kO();function xe(n,t,e=zO,i){const a=i?.getOnInit,o=zt(a?e.getItem(n,t):t);return(P2?"production":void 0)!=="production"&&(o.debugPrivate=!0),o.onMount=u=>{var f;return u(e.getItem(n,t)),(f=e.subscribe)==null?void 0:f.call(e,n,u,t)},zt(u=>u(o),(u,f,d)=>{const m=typeof d=="function"?d(u(o)):d;return m===UO?(f(o,t),e.removeItem(n)):lb(m)?m.then(g=>(f(o,g),e.setItem(n,g))):(f(o,m),e.setItem(n,m))})}globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const VO=n=>{const t=Re.c(10);let e,i;t[0]!==n?({fftData:e,...i}=n,t[0]=n,t[1]=e,t[2]=i):(e=t[1],i=t[2]);const a=q.useRef(null),o=q.useRef(void 0);o.current===void 0&&(o.current=e);let c,u;t[3]!==e?(c=()=>{o.current=e},u=[e],t[3]=e,t[4]=c,t[5]=u):(c=t[4],u=t[5]),q.useEffect(c,u);let f,d;t[6]===Symbol.for("react.memo_cache_sentinel")?(f=()=>{const g=a.current;if(g){const x=g.getContext("2d");if(x){let P=function(){if(!(g&&x)||S)return;const w=g.width,F=g.height;(b.width!==w||b.height!==F)&&(g.width=b.width,g.height=b.height);const E=o.current??[];if(L.length!==E.length)L=[...E];else for(let A=0;A0?Math.min(L.length,k):0,G=U>0?R(L,U):[],J=G.length>0?Math.max.apply(Math,G):0;T=Math.max(J*.1+T*.9,100);const tt=w/Math.max(1,G.length);x.strokeStyle="white",x.lineWidth=4*O,x.lineCap="round",x.lineJoin="round";const K=F-tt;for(let A=0;A{for(const F of w)b={width:F.contentRect.width*window.devicePixelRatio,height:F.contentRect.height*window.devicePixelRatio}});C.observe(g);let T=100,S=!1,L=[];const R=function(F,E){const O=F.length;if(E<=0||O===0)return[];if(O===E)return F.slice();const I=new Array(E),k=(O-1)/Math.max(1,E-1);for(let U=0;U{C.disconnect(),S=!0}}}},d=[],t[6]=f,t[7]=d):(f=t[6],d=t[7]),q.useLayoutEffect(f,d);let m;return t[8]!==i?(m=Y.jsx("canvas",{ref:a,...i}),t[8]=i,t[9]=m):m=t[9],m},GO=(n,t)=>Y.jsx("svg",{width:192,height:36,viewBox:"0 0 192 36",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M139.11 15.884H135.582V23.8C135.582 24.6027 135.732 25.1814 136.03 25.536C136.329 25.872 136.805 26.04 137.458 26.04C137.701 26.04 137.962 26.012 138.242 25.956C138.522 25.9 138.774 25.816 138.998 25.704L139.082 27.86C138.765 27.972 138.41 28.056 138.018 28.112C137.645 28.1867 137.253 28.224 136.842 28.224C135.592 28.224 134.63 27.8787 133.958 27.188C133.305 26.4974 132.978 25.4614 132.978 24.08V15.884H130.43V13.728H132.978V7.92004H135.582V13.728H139.11V15.884ZM117.188 20.692H124.608L120.912 11.396L117.188 20.692ZM114.22 28H111.14L119.736 8.17604H122.256L130.768 28H127.632L125.616 23.128H116.208L114.22 28ZM145.441 15.12C144.713 15.6054 144.181 16.2027 143.845 16.912H143.761C143.761 16.632 143.752 16.2867 143.733 15.876C143.715 15.4467 143.687 15.064 143.649 14.728H141.157C141.195 15.1574 141.223 15.652 141.241 16.212C141.26 16.772 141.269 17.2294 141.269 17.584V28H143.901V20.944C143.901 19.6187 144.219 18.564 144.853 17.78C145.507 16.9774 146.319 16.576 147.289 16.576C147.812 16.576 148.251 16.6694 148.605 16.856C148.96 17.024 149.249 17.2667 149.473 17.584C149.697 17.8827 149.856 18.2467 149.949 18.676C150.043 19.1054 150.089 19.5814 150.089 20.104V28H152.721V20.832C152.721 20.3094 152.777 19.796 152.889 19.292C153.02 18.788 153.216 18.34 153.477 17.948C153.757 17.5374 154.112 17.2107 154.541 16.968C154.971 16.7067 155.484 16.576 156.081 16.576C156.66 16.576 157.136 16.688 157.509 16.912C157.883 17.136 158.181 17.4347 158.405 17.808C158.629 18.1814 158.788 18.6107 158.881 19.096C158.975 19.5814 159.021 20.0947 159.021 20.636V28H161.653V19.768C161.653 19.0214 161.551 18.3214 161.345 17.668C161.159 17.0147 160.86 16.4454 160.449 15.96C160.039 15.4747 159.525 15.092 158.909 14.812C158.293 14.5134 157.556 14.364 156.697 14.364C155.745 14.364 154.868 14.5974 154.065 15.064C153.263 15.512 152.637 16.1654 152.189 17.024C151.779 16.072 151.209 15.3907 150.481 14.98C149.772 14.5694 148.969 14.364 148.073 14.364C147.065 14.364 146.188 14.616 145.441 15.12ZM176.233 21.336C176.233 20.7014 176.131 20.0947 175.925 19.516C175.739 18.9374 175.459 18.4334 175.085 18.004C174.712 17.556 174.245 17.2014 173.685 16.94C173.144 16.6787 172.519 16.548 171.809 16.548C171.1 16.548 170.465 16.6787 169.905 16.94C169.364 17.2014 168.907 17.556 168.533 18.004C168.16 18.4334 167.871 18.9374 167.665 19.516C167.479 20.0947 167.385 20.7014 167.385 21.336C167.385 21.9707 167.479 22.5774 167.665 23.156C167.871 23.7347 168.16 24.248 168.533 24.696C168.907 25.144 169.364 25.4987 169.905 25.76C170.465 26.0214 171.1 26.152 171.809 26.152C172.519 26.152 173.144 26.0214 173.685 25.76C174.245 25.4987 174.712 25.144 175.085 24.696C175.459 24.248 175.739 23.7347 175.925 23.156C176.131 22.5774 176.233 21.9707 176.233 21.336ZM178.977 21.336C178.977 22.3627 178.791 23.3054 178.417 24.164C178.063 25.0227 177.559 25.7694 176.905 26.404C176.271 27.02 175.515 27.5054 174.637 27.86C173.76 28.196 172.817 28.364 171.809 28.364C170.801 28.364 169.859 28.196 168.981 27.86C168.104 27.5054 167.348 27.02 166.713 26.404C166.079 25.7694 165.575 25.0227 165.201 24.164C164.847 23.3054 164.669 22.3627 164.669 21.336C164.669 20.3094 164.847 19.376 165.201 18.536C165.575 17.6774 166.079 16.94 166.713 16.324C167.348 15.708 168.104 15.232 168.981 14.896C169.859 14.5414 170.801 14.364 171.809 14.364C172.817 14.364 173.76 14.5414 174.637 14.896C175.515 15.232 176.271 15.708 176.905 16.324C177.559 16.94 178.063 17.6774 178.417 18.536C178.791 19.376 178.977 20.3094 178.977 21.336ZM187.975 16.912C188.517 17.2107 188.937 17.6027 189.235 18.088L191.055 16.548C190.57 15.8387 189.879 15.2974 188.983 14.924C188.106 14.5507 187.191 14.364 186.239 14.364C185.605 14.364 184.989 14.448 184.391 14.616C183.794 14.7654 183.262 15.008 182.795 15.344C182.329 15.6614 181.946 16.0814 181.647 16.604C181.367 17.108 181.227 17.7147 181.227 18.424C181.227 19.04 181.339 19.5627 181.563 19.992C181.806 20.4027 182.114 20.748 182.487 21.028C182.861 21.308 183.271 21.532 183.719 21.7C184.186 21.8494 184.643 21.98 185.091 22.092C186.23 22.3534 187.089 22.6334 187.667 22.932C188.265 23.2307 188.563 23.688 188.563 24.304C188.563 24.6774 188.489 24.9854 188.339 25.228C188.19 25.4707 187.985 25.6667 187.723 25.816C187.481 25.9654 187.201 26.0774 186.883 26.152C186.585 26.208 186.267 26.236 185.931 26.236C185.185 26.236 184.494 26.0587 183.859 25.704C183.243 25.3494 182.758 24.9107 182.403 24.388L180.527 25.984C181.162 26.7867 181.955 27.384 182.907 27.776C183.878 28.168 184.867 28.364 185.875 28.364C186.547 28.364 187.201 28.2894 187.835 28.14C188.47 27.9907 189.03 27.748 189.515 27.412C190.019 27.0574 190.421 26.6094 190.719 26.068C191.018 25.5267 191.167 24.864 191.167 24.08C191.167 23.5014 191.055 22.9974 190.831 22.568C190.607 22.1387 190.29 21.7747 189.879 21.476C189.487 21.1587 189.021 20.8974 188.479 20.692C187.938 20.4867 187.35 20.3094 186.715 20.16C185.707 19.936 184.951 19.6934 184.447 19.432C183.943 19.152 183.691 18.732 183.691 18.172C183.691 17.8547 183.757 17.5934 183.887 17.388C184.037 17.164 184.223 16.9867 184.447 16.856C184.69 16.7067 184.961 16.604 185.259 16.548C185.558 16.4734 185.857 16.436 186.155 16.436C186.827 16.436 187.434 16.5947 187.975 16.912ZM2.95973 27.9533H0V7.93701H2.95973C8.48624 7.93701 12.9927 12.4323 12.9927 17.9451C12.9927 23.4579 8.48624 27.9533 2.95973 27.9533ZM25.5841 7.93701H28.5439V27.9533H25.5841C20.0576 27.9533 15.5511 23.4579 15.5511 17.9451C15.5511 12.4323 20.0576 7.93701 25.5841 7.93701ZM34.6222 7.93701H41.8682C47.4003 7.93701 51.9012 12.4268 51.9012 17.9451C51.9012 23.4635 47.4003 27.9533 41.8682 27.9533H34.6222V7.93701ZM37.6627 24.9175H41.8682C45.717 24.9175 48.8579 21.7871 48.8579 17.9424C48.8579 14.1031 45.7198 10.97 41.8682 10.97H37.6627V24.9175ZM53.4424 21.0365C53.4424 17.2112 56.561 14.1003 60.3958 14.1003C64.2307 14.1003 67.3492 17.2112 67.3492 21.0365C67.3492 24.8619 64.2307 27.9727 60.3958 27.9727C56.561 27.9727 53.4424 24.8619 53.4424 21.0365ZM56.2043 21.0115C56.2043 23.305 58.0687 25.1927 60.3958 25.1927C62.6978 25.1927 64.5874 23.3301 64.5874 21.0115C64.5874 18.718 62.695 16.8303 60.3958 16.8303C58.0966 16.8303 56.2043 18.6902 56.2043 21.0115ZM73.3574 27.9755H70.3168V7.93979H73.3574V27.9755ZM79.9647 15.3514C81.0906 14.5646 82.4618 14.1003 83.9389 14.1003C87.7709 14.1003 90.8895 17.2112 90.8923 21.0365C90.8923 24.8619 87.7737 27.9727 83.9389 27.9727C82.4618 27.9727 81.0934 27.5084 79.9647 26.7217V27.9755H76.9158V7.93979H79.9647V15.3514ZM79.9647 22.3292C80.5165 23.9861 82.08 25.1927 83.9389 25.1927C86.2381 25.1927 88.1304 23.3328 88.1304 21.0115C88.1304 18.718 86.2381 16.8303 83.9389 16.8303C82.0995 16.8303 80.5221 18.023 79.9647 19.691C79.8254 20.1052 79.7473 20.55 79.7473 21.0115C79.7473 21.473 79.8254 21.915 79.9647 22.3292ZM99.0715 22.9937L103.035 14.1059H106.357C105.983 14.9363 103.631 20.1804 101.593 24.7238C99.933 28.4242 98.4816 31.6599 98.4778 31.6674C97.5944 33.6468 95.2589 34.5419 93.283 33.6607L92.2295 33.1908L92.224 33.1881L93.453 30.4386L93.8905 30.6332C94.7043 30.9974 95.6658 30.6276 96.0309 29.8131C96.0337 29.8048 97.4104 26.7161 97.4104 26.7161L91.7864 14.1059H95.1084L99.0715 22.9937Z",fill:"currentColor"})}),E8=q.forwardRef(GO),jO=(n,t)=>Y.jsxs("svg",{width:30,height:20,viewBox:"0 0 30 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:[Y.jsx("g",{clipPath:"url(#clip0_207_8)",children:Y.jsx("path",{d:"M16.0117 1.81812C18.4258 1.81812 19.8776 4.78099 20.958 7.92092L21.0631 8.23024L21.1659 8.54011L21.2666 8.84995C21.5987 9.88202 21.8972 10.9037 22.1828 11.8167C22.839 8.96008 22.4949 8.34671 23.1979 8.34671C23.4594 8.34671 23.724 8.53256 23.724 8.87123C23.724 9.0083 23.5105 10.461 23.2136 11.9166L23.1588 12.1809L23.1023 12.4436C23.0068 12.8791 22.9051 13.3002 22.8009 13.6704C25.8224 21.9469 28.0659 11.0453 28.3194 8.81691C28.3564 8.49556 28.5879 8.34672 28.8219 8.34672C29.1344 8.34672 29.3907 8.59283 29.3418 8.96318C28.8009 12.5671 27.9689 18.1818 24.5482 18.1818C22.6604 18.1818 21.5992 16.6374 20.7611 14.8678C20.179 13.6722 19.6841 12.2511 19.2288 10.8221L19.1203 10.4791C19.0844 10.3648 19.0487 10.2505 19.0132 10.1364L18.9073 9.79479C17.8713 6.44275 16.9926 3.33702 15.6457 3.33702C14.9875 3.33702 14.4995 4.05555 14.4657 4.05555C14.4036 4.05555 14.3548 3.76279 13.7401 2.96069C14.3251 2.26231 15.1314 1.81812 16.0117 1.81812ZM4.80934 1.82692C10.1957 1.82692 10.6747 16.6849 13.7232 16.6849C14.0811 16.6849 14.4669 16.4581 14.8886 15.9443C15.122 16.3577 15.3613 16.7247 15.6089 17.0478C15.0004 17.774 14.2433 18.1812 13.2952 18.1812C9.88906 18.1807 8.42559 12.2177 7.16605 8.19178C6.86941 9.48319 6.71544 10.6519 6.65516 11.1766C6.61775 11.5101 6.38397 11.662 6.14846 11.662C5.88728 11.662 5.62398 11.4752 5.62398 11.1416C5.62398 11.1167 5.62545 11.091 5.62848 11.0646C5.80292 9.74118 6.15443 7.73614 6.54802 6.3382C6.02411 4.90309 5.36971 3.33458 4.40561 3.33458C2.45924 3.33458 1.34461 8.44844 1.03117 11.1766C0.993764 11.5101 0.759979 11.662 0.524468 11.662C0.263303 11.662 0 11.4752 0 11.1416C0 11.1167 0.00146422 11.091 0.00449843 11.0646C0.0496669 10.7219 0.0980906 10.373 0.15073 10.0213L0.196904 9.71916C0.204779 9.66871 0.212746 9.61823 0.220807 9.56772L0.27033 9.26437C0.887195 5.57133 2.03712 1.82692 4.80934 1.82692ZM10.3981 1.81878C11.3654 1.81878 12.4031 2.34503 13.2976 3.57969C13.3476 3.63964 13.849 4.45427 14.0072 4.73669C14.554 5.77231 15.0302 7.038 15.4694 8.354L15.5832 8.69833C16.8447 12.5534 17.8174 16.7028 19.3326 16.7028C19.6918 16.7028 20.0814 16.4697 20.5126 15.9442C20.746 16.3575 20.9852 16.7245 21.2329 17.0476C20.6259 17.7724 19.8691 18.1812 18.9192 18.1812C13.565 18.1804 12.983 3.3313 10.0333 3.3313C9.36758 3.3313 8.87559 4.05559 8.84162 4.05559C8.7795 4.05559 8.7307 3.76287 8.11611 2.96083C8.7168 2.24348 9.52906 1.81878 10.3981 1.81878Z",fill:"currentColor"})}),Y.jsx("defs",{children:Y.jsx("clipPath",{id:"clip0_207_8",children:Y.jsx("rect",{width:30,height:20,fill:"currentColor"})})})]}),HO=q.forwardRef(jO);var wa={audioQualityTag:"nNqm_a_audioQualityTag",clickable:"nNqm_a_clickable",commonTag:"nNqm_a_commonTag",commonTagText:"nNqm_a_commonTagText",dolbyLogoGlow:"nNqm_a_dolbyLogoGlow"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const qO={hide:{opacity:0,scale:.8,transition:{duration:.25,ease:"circIn"}},show:{opacity:1,scale:1,transition:{duration:1,ease:[0,.71,.2,1.01]}},hover:{scale:.95},active:{scale:.9}},XO={hide:{opacity:0,scale:.8,transition:{duration:.25,ease:"circIn"}},show:{opacity:[0,1],scale:1,transition:{duration:1,ease:[0,.71,.2,1.01]}},hover:{scale:.95},active:{scale:.9}},YO=q.memo(q.forwardRef((n,t)=>{const e=Re.c(21);let i,a,o,c,u,f;e[0]!==n?({tagText:f,tagIcon:u,isDolbyAtmos:a,className:i,onClick:o,...c}=n,e[0]=n,e[1]=i,e[2]=a,e[3]=o,e[4]=c,e[5]=u,e[6]=f):(i=e[1],a=e[2],o=e[3],c=e[4],u=e[5],f=e[6]);const d=o&&wa.clickable;let m;e[7]!==i||e[8]!==d?(m=on(i,wa.audioQualityTag,d),e[7]=i,e[8]=d,e[9]=m):m=e[9];let g;e[10]!==a||e[11]!==o||e[12]!==u||e[13]!==f?(g=a?Y.jsxs(Kn.div,{initial:"hide",animate:"show",whileHover:o?"hover":void 0,whileTap:o?"active":void 0,exit:"hide",className:wa.dolbyLogo,variants:XO,children:[Y.jsx(E8,{className:wa.dolbyLogoGlow}),Y.jsx(E8,{})]},"dolby-atmos"):Y.jsx(Kn.div,{initial:"hide",animate:"show",whileHover:o?"hover":void 0,whileTap:o?"active":void 0,exit:"hide",variants:qO,children:Y.jsxs("div",{className:wa.commonTag,children:[u&&Y.jsx(HO,{height:"11px"}),f&&Y.jsx("div",{className:wa.commonTagText,children:f})]})},`common-tag-${u}-${f}`),e[10]=a,e[11]=o,e[12]=u,e[13]=f,e[14]=g):g=e[14];let v;return e[15]!==o||e[16]!==t||e[17]!==c||e[18]!==m||e[19]!==g?(v=Y.jsx("div",{className:m,onClick:o,ref:t,...c,children:g}),e[15]=o,e[16]=t,e[17]=c,e[18]=m,e[19]=g,e[20]=v):v=e[20],v}));var P0={inner:"ah9gFq_inner",nowPlayingSlider:"ah9gFq_nowPlayingSlider",thumb:"ah9gFq_thumb"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const w8=12/2,M8=12,cb=({className:n,style:t,value:e,min:i,max:a,isPlaying:o=!1,onChange:c,onAfterChange:u,onBeforeChange:f,onSeeking:d,beforeIcon:m,afterIcon:g,changeOnDrag:v=!1})=>{const x=q.useRef(null),b=q.useRef(null),C=q.useRef(null),T=q.useRef(!1),S=oh(0),L=T2(S,[0,1],[0,1]),R=oh(w8),P=Hw`inset(${R}px 0px round 100px)`,w=$w(0,{damping:12,stiffness:300}),F=q.useRef(!1),E=q.useRef(e);q.useEffect(()=>{if(F.current)return;E.current=e;const A=Math.max(0,Math.min(1,(e-i)/(a-i)));S.set(A)},[e,i,a,S]),Ww((A,B)=>{if(o&&!F.current){E.current+=B,E.current>a&&(E.current=a);const N=Math.max(0,Math.min(1,(E.current-i)/(a-i)));S.set(N)}});const O=()=>{I5(R,0,{type:"tween",ease:"easeOut",duration:.28})},I=()=>{I5(R,w8,{type:"spring",damping:12,stiffness:200})},k=A=>{F.current=!0,b.current&&(C.current=b.current.getBoundingClientRect()),O(),f?.(),d?.(!0)},U=(A,B)=>{const N=C.current;if(!N)return;const H=(B.point.x-N.left)/N.width;H<0?w.set(Math.tanh(H*2)*M8):H>1?w.set(Math.tanh((H-1)*2)*M8):w.set(0);const V=Math.max(0,Math.min(1,H)),M=i+V*(a-i);E.current=M,S.set(V),v&&c?.(M)},G=()=>{F.current=!1,C.current=null,T.current?O():I(),w.set(0),d?.(!1),c?.(E.current),u?.(E.current)},J=()=>{T.current=!0,F.current||O()},tt=()=>{T.current=!1,F.current||I()},K=(A,B)=>{const N=b.current?.getBoundingClientRect();if(!N)return;const H=Math.max(0,Math.min(1,(B.point.x-N.left)/N.width)),V=i+H*(a-i);E.current=V,S.set(H),f?.(),c?.(V),u?.(V)};return Y.jsxs(Kn.div,{ref:x,className:on(P0.nowPlayingSlider,n),style:{...t,x:w},onPanStart:k,onPan:U,onPanEnd:G,onTap:K,onHoverStart:J,onHoverEnd:tt,children:[m,Y.jsx(Kn.div,{ref:b,className:P0.inner,style:{clipPath:P},children:Y.jsx(Kn.div,{className:P0.thumb,style:{scaleX:L,originX:0}})}),g]})};var $O={controlThumb:"OWkF6W_controlThumb"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const WO=n=>{const t=Re.c(25);let e,i,a;t[0]!==n?({onClick:i,className:e,...a}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a):(e=t[1],i=t[2],a=t[3]);const o=q.useRef(null),c=q.useRef(!1);let u;t[4]===Symbol.for("react.memo_cache_sentinel")?(u={x:-25,y:-4},t[4]=u):u=t[4];const[f,d]=q.useState(u);let m;t[5]===Symbol.for("react.memo_cache_sentinel")?(m=O=>{const I=o.current;if(I&&c.current){const k=I.getBoundingClientRect(),U=(O.clientX-k.left)/2,G=(O.clientY-k.top)/2;Math.abs(U)>25||Math.abs(G)>25?d({x:-25,y:-4}):d({x:U-12.5,y:G-12.5})}},t[5]=m):m=t[5];const g=m;let v;t[6]!==e?(v=on($O.controlThumb,e),t[6]=e,t[7]=v):v=t[7];let x;t[8]===Symbol.for("react.memo_cache_sentinel")?(x={rest:{width:50,height:8},hover:{width:25,height:25}},t[8]=x):x=t[8];let b;t[9]!==f?(b={...f},t[9]=f,t[10]=b):b=t[10];let C;t[11]===Symbol.for("react.memo_cache_sentinel")?(C={scale:.9},t[11]=C):C=t[11];let T,S,L,R;t[12]===Symbol.for("react.memo_cache_sentinel")?(S={type:"spring",duration:.5},L=O=>{g(O.nativeEvent)},R=O=>{g(O),c.current=!0},T=()=>{c.current=!1,d({x:-25,y:-4})},t[12]=T,t[13]=S,t[14]=L,t[15]=R):(T=t[12],S=t[13],L=t[14],R=t[15]);let P;t[16]===Symbol.for("react.memo_cache_sentinel")?(P=Y.jsx(Kn.div,{variants:{rest:{height:0,width:0,marginTop:0,marginLeft:25,rotate:0},hover:{height:2,width:15,marginTop:-1,marginLeft:5,rotate:45}},transition:{type:"spring",duration:.5}}),t[16]=P):P=t[16];let w;t[17]===Symbol.for("react.memo_cache_sentinel")?(w=Y.jsx(Kn.div,{variants:{rest:{height:0,width:0,marginTop:0,marginLeft:25,rotate:0},hover:{height:2,width:15,marginTop:-1,marginLeft:5,rotate:-45}},transition:{type:"spring",duration:.5}}),t[17]=w):w=t[17];let F;t[18]!==i||t[19]!==b?(F=Y.jsxs(Kn.button,{type:"button",variants:x,animate:b,whileTap:C,whileHover:"hover",initial:"rest",transition:S,onMouseMove:L,onHoverStart:R,onHoverEnd:T,onClick:i,children:[P,w]}),t[18]=i,t[19]=b,t[20]=F):F=t[20];let E;return t[21]!==a||t[22]!==F||t[23]!==v?(E=Y.jsx("div",{className:v,ref:o,...a,children:F}),t[21]=a,t[22]=F,t[23]=v,t[24]=E):E=t[24],E};var ol={cover:"Q2PpKa_cover",coverInner:"Q2PpKa_coverInner",musicPaused:"Q2PpKa_musicPaused"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const ZO=q.forwardRef((n,t)=>{const e=Re.c(33);let i,a,o,c,u,f,d;e[0]!==n?({coverUrl:o,coverIsVideo:a,coverVideoPaused:c,className:i,musicPaused:u,pauseShrinkAspect:f,...d}=n,e[0]=n,e[1]=i,e[2]=a,e[3]=o,e[4]=c,e[5]=u,e[6]=f,e[7]=d):(i=e[1],a=e[2],o=e[3],c=e[4],u=e[5],f=e[6],d=e[7]);const m=q.useRef(null),g=u&&ol.musicPaused;let v;e[8]!==i||e[9]!==g?(v=on(ol.cover,g,i),e[8]=i,e[9]=g,e[10]=v):v=e[10];const x=v,b=q.useRef(null);let C,T;e[11]!==c?(C=()=>{const G=b.current;G&&(c?G.pause():G.play())},T=[c],e[11]=c,e[12]=C,e[13]=T):(C=e[12],T=e[13]),q.useEffect(C,T);const[S,L]=q.useState(20);let R,P;e[14]===Symbol.for("react.memo_cache_sentinel")?(R=()=>{const G=m.current;if(G){const J=()=>{const K=Math.min(G.clientWidth,G.clientHeight);L(Math.max(K*.02,window.innerHeight*.007))},tt=new ResizeObserver(J);return J(),tt.observe(G),()=>{tt.disconnect()}}},P=[],e[14]=R,e[15]=P):(R=e[14],P=e[15]),q.useLayoutEffect(R,P);const w=f??.75;let F;e[16]!==w?(F={"--scale-level":w},e[16]=w,e[17]=F):F=e[17];const E=F;let O;e[18]!==t?(O=G=>{m.current=G,typeof t=="function"?t(G):t&&(t.current=G)},e[18]=t,e[19]=O):O=e[19];let I;e[20]!==a||e[21]!==o||e[22]!==f?(I=a?Y.jsx("video",{className:ol.coverInner,src:o,autoPlay:!0,loop:!0,muted:!0,playsInline:!0,crossOrigin:"anonymous",ref:b}):Y.jsx("div",{className:ol.coverInner,style:{backgroundImage:`url(${o})`,"--scale-level":f??.75}}),e[20]=a,e[21]=o,e[22]=f,e[23]=I):I=e[23];let k;e[24]!==S||e[25]!==I?(k=Y.jsx(AM,{cornerRadius:S,cornerSmoothing:.7,className:ol.coverInner,children:I}),e[24]=S,e[25]=I,e[26]=k):k=e[26];let U;return e[27]!==x||e[28]!==d||e[29]!==O||e[30]!==k||e[31]!==E?(U=Y.jsx("div",{className:x,style:E,ref:O,...d,children:k}),e[27]=x,e[28]=d,e[29]=O,e[30]=k,e[31]=E,e[32]=U):U=e[32],U});var R8={animate:"_7JvWRa_animate",mediaButton:"_7JvWRa_mediaButton"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const ll=q.memo(n=>{const t=Re.c(21);let e,i,a,o,c,u;t[0]!==n?({className:i,children:e,type:u,onClick:o,onAnimationEnd:a,...c}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o,t[5]=c,t[6]=u):(e=t[1],i=t[2],a=t[3],o=t[4],c=t[5],u=t[6]);const f=u===void 0?"button":u,[d,m]=q.useState(!1);let g;t[7]!==o?(g=S=>{m(!0),o?.(S)},t[7]=o,t[8]=g):g=t[8];const v=g;let x;t[9]!==a?(x=S=>{m(!1),a?.(S)},t[9]=a,t[10]=x):x=t[10];const b=x;let C;t[11]!==i||t[12]!==d?(C=on(R8.mediaButton,{[R8.animate]:d},i),t[11]=i,t[12]=d,t[13]=C):C=t[13];let T;return t[14]!==e||t[15]!==b||t[16]!==v||t[17]!==c||t[18]!==C||t[19]!==f?(T=Y.jsx("button",{className:C,type:f,onClick:v,onAnimationEnd:b,...c,children:e}),t[14]=e,t[15]=b,t[16]=v,t[17]=c,t[18]=C,t[19]=f,t[20]=T):T=t[20],T}),KO=(n,t)=>Y.jsx("svg",{id:"vector",xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",ref:t,...n,children:Y.jsx("path",{d:"M5.5161,14.1665C5.8687,14.1665 6.1856,14.083 6.467,13.916C6.7485,13.749 6.9742,13.5264 7.1443,13.248C7.3144,12.9697 7.3994,12.6574 7.3994,12.311C7.3994,11.7977 7.217,11.3601 6.8521,10.9983C6.4871,10.6365 6.0418,10.4556 5.5161,10.4556C5.1759,10.4556 4.8652,10.5391 4.5837,10.7061C4.3023,10.873 4.0781,11.0972 3.9111,11.3787C3.7441,11.6601 3.6606,11.9709 3.6606,12.311C3.6606,12.6574 3.7441,12.9697 3.9111,13.248C4.0781,13.5264 4.3023,13.749 4.5837,13.916C4.8652,14.083 5.1759,14.1665 5.5161,14.1665ZM12.4092,14.1665C12.7555,14.1665 13.0679,14.083 13.3462,13.916C13.6245,13.749 13.8472,13.5264 14.0142,13.248C14.1812,12.9697 14.2646,12.6574 14.2646,12.311C14.2646,11.7977 14.0837,11.3601 13.7219,10.9983C13.3601,10.6365 12.9225,10.4556 12.4092,10.4556C12.069,10.4556 11.7582,10.5391 11.4768,10.7061C11.1954,10.873 10.9727,11.0972 10.8088,11.3787C10.6449,11.6601 10.563,11.9709 10.563,12.311C10.563,12.6574 10.6449,12.9697 10.8088,13.248C10.9727,13.5264 11.1954,13.749 11.4768,13.916C11.7582,14.083 12.069,14.1665 12.4092,14.1665ZM19.3022,14.1665C19.6424,14.1665 19.9532,14.083 20.2346,13.916C20.516,13.749 20.7402,13.5264 20.9072,13.248C21.0742,12.9697 21.1577,12.6574 21.1577,12.311C21.1577,11.7977 20.9768,11.3601 20.615,10.9983C20.2532,10.6365 19.8156,10.4556 19.3022,10.4556C18.9559,10.4556 18.6405,10.5391 18.356,10.7061C18.0715,10.873 17.8457,11.0972 17.6787,11.3787C17.5117,11.6601 17.4282,11.9709 17.4282,12.311C17.4282,12.6574 17.5117,12.9697 17.6787,13.248C17.8457,13.5264 18.0715,13.749 18.356,13.916C18.6405,14.083 18.9559,14.1665 19.3022,14.1665Z",fill:"currentColor",fillRule:"nonzero",id:"path_0"})}),QO=q.forwardRef(KO);var JO={menuButton:"GOs7HW_menuButton"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const tI=q.memo(n=>{const t=Re.c(9);let e,i;if(t[0]!==n){const{className:u,type:f,...d}=n;e=u,i=d,t[0]=n,t[1]=e,t[2]=i}else e=t[1],i=t[2];let a;t[3]!==e?(a=on(JO.menuButton,e),t[3]=e,t[4]=a):a=t[4];let o;t[5]===Symbol.for("react.memo_cache_sentinel")?(o=Y.jsx(QO,{}),t[5]=o):o=t[5];let c;return t[6]!==i||t[7]!==a?(c=Y.jsx("button",{className:a,type:"button",...i,children:o}),t[6]=i,t[7]=a,t[8]=c):c=t[8],c});var Du={animating:"Dbqjoq_animating",textMarquee:"Dbqjoq_textMarquee"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const F0=q.memo(n=>{const t=Re.c(15);let e,i,a;t[0]!==n?({children:e,className:i,...a}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a):(e=t[1],i=t[2],a=t[3]);const o=q.useRef(null),c=q.useRef(null);let u;t[4]===Symbol.for("react.memo_cache_sentinel")?(u=new Set,t[4]=u):u=t[4];const f=q.useRef(u);let d;t[5]===Symbol.for("react.memo_cache_sentinel")?(d=()=>{if(c.current&&o.current){const T=o.current.clientWidth,S=c.current.clientWidth;if(S<=T*.95)return;o.current?.classList.add(Du.animating);const L=S-T*.95,R=c.current.animate([{transform:"translateX(0px)"},{transform:`translateX(${-L}px)`}],{iterations:2,direction:"alternate",easing:"linear",duration:Math.max(0,L*2/64*1e3)});R.finished.then(()=>{o.current?.classList.remove(Du.animating)}),f.current.add(R)}},t[5]=d):d=t[5];const m=d;let g;t[6]===Symbol.for("react.memo_cache_sentinel")?(g=()=>{for(const T of f.current)T.finish();o.current?.classList.remove(Du.animating),f.current.clear()},t[6]=g):g=t[6];const v=g;let x;t[7]!==i?(x=on(Du.textMarquee,i),t[7]=i,t[8]=x):x=t[8];let b;t[9]!==e?(b=Y.jsx("div",{ref:c,children:e}),t[9]=e,t[10]=b):b=t[10];let C;return t[11]!==a||t[12]!==x||t[13]!==b?(C=Y.jsx("div",{ref:o,className:x,onMouseEnter:m,onMouseLeave:v,...a,children:b}),t[11]=a,t[12]=x,t[13]=b,t[14]=C):C=t[14],C});var cl={album:"XWzuDW_album",artists:"XWzuDW_artists",info:"XWzuDW_info",musicInfo:"XWzuDW_musicInfo",name:"XWzuDW_name"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const eI=q.memo(n=>{const t=Re.c(26);let e,i,a,o,c,u;if(t[0]!==n){const{name:C,artists:T,album:S,onArtistClicked:L,onAlbumClicked:R,onMenuButtonClicked:P,className:w,...F}=n;o=C,i=T,e=S,c=P,a=w,u=F,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o,t[5]=c,t[6]=u}else e=t[1],i=t[2],a=t[3],o=t[4],c=t[5],u=t[6];let f;t[7]!==a?(f=on(cl.musicInfo,a),t[7]=a,t[8]=f):f=t[8];let d;t[9]!==o?(d=o!==void 0&&Y.jsx(F0,{className:cl.name,children:o}),t[9]=o,t[10]=d):d=t[10];let m;t[11]!==i?(m=i!==void 0&&Y.jsx(F0,{className:cl.artists,children:i.map(nI)}),t[11]=i,t[12]=m):m=t[12];let g;t[13]!==e?(g=e!==void 0&&Y.jsx(F0,{className:cl.album,children:e}),t[13]=e,t[14]=g):g=t[14];let v;t[15]!==d||t[16]!==m||t[17]!==g?(v=Y.jsxs("div",{className:cl.info,children:[d,m,g]}),t[15]=d,t[16]=m,t[17]=g,t[18]=v):v=t[18];let x;t[19]!==c?(x=Y.jsx(tI,{onClick:c}),t[19]=c,t[20]=x):x=t[20];let b;return t[21]!==u||t[22]!==f||t[23]!==v||t[24]!==x?(b=Y.jsxs("div",{className:f,...u,children:[v,x]}),t[21]=u,t[22]=f,t[23]=v,t[24]=x,t[25]=b):b=t[25],b});function nI(n){return Y.jsx("a",{children:n},`artist-${n}`)}var iI={background:"_8f42YG_background"},dr={bottomControls:"-DaA0W_bottomControls",controls:"-DaA0W_controls",cover:"-DaA0W_cover",hideLyric:"-DaA0W_hideLyric",horizontalLayout:"-DaA0W_horizontalLayout",lyric:"-DaA0W_lyric",thumb:"-DaA0W_thumb"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const sI=n=>{const t=Re.c(32);let e,i,a,o,c,u,f,d,m;t[0]!==n?({thumbSlot:m,coverSlot:c,controlsSlot:o,lyricSlot:f,bottomControls:i,hideLyric:u,className:a,asChild:e,...d}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o,t[5]=c,t[6]=u,t[7]=f,t[8]=d,t[9]=m):(e=t[1],i=t[2],a=t[3],o=t[4],c=t[5],u=t[6],f=t[7],d=t[8],m=t[9]);const g=!e&&dr.horizontalLayout,v=!e&&u&&dr.hideLyric;let x;t[10]!==a||t[11]!==g||t[12]!==v?(x=on(a,g,v),t[10]=a,t[11]=g,t[12]=v,t[13]=x):x=t[13];let b;t[14]!==m?(b=Y.jsx(Kn.div,{layout:!0,layoutId:"amll-player-thumb",className:dr.thumb,children:m}),t[14]=m,t[15]=b):b=t[15];let C;t[16]!==c?(C=Y.jsx(Kn.div,{layout:!0,layoutId:"amll-player-cover",className:dr.cover,children:c}),t[16]=c,t[17]=C):C=t[17];let T;t[18]!==o?(T=Y.jsx(Kn.div,{layout:!0,layoutId:"amll-player-controls",className:dr.controls,children:o}),t[18]=o,t[19]=T):T=t[19];let S;t[20]!==f?(S=Y.jsx("div",{className:dr.lyric,children:f}),t[20]=f,t[21]=S):S=t[21];let L;t[22]!==i?(L=Y.jsx("div",{className:dr.bottomControls,children:i}),t[22]=i,t[23]=L):L=t[23];let R;return t[24]!==d||t[25]!==x||t[26]!==b||t[27]!==C||t[28]!==T||t[29]!==S||t[30]!==L?(R=Y.jsxs("div",{className:x,...d,children:[b,C,T,S,L]}),t[24]=d,t[25]=x,t[26]=b,t[27]=C,t[28]=T,t[29]=S,t[30]=L,t[31]=R):R=t[31],R};var qn={bigControls:"SqBoeq_bigControls",coverFrame:"SqBoeq_coverFrame",hideLyric:"SqBoeq_hideLyric",immerseCover:"SqBoeq_immerseCover",lyric:"SqBoeq_lyric",lyricLayout:"SqBoeq_lyricLayout",noLyricLayout:"SqBoeq_noLyricLayout",phonyBigCover:"SqBoeq_phonyBigCover",phonySmallCover:"SqBoeq_phonySmallCover",smallControls:"SqBoeq_smallControls",thumb:"SqBoeq_thumb",verticalLayout:"SqBoeq_verticalLayout"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const rI=({thumbSlot:n,coverSlot:t,smallControlsSlot:e,bigControlsSlot:i,lyricSlot:a,hideLyric:o,asChild:c,className:u,immerseCover:f,...d})=>{const m=q.useRef(null),g=q.useRef(null),v=q.useRef(null),x=q.useRef(null),b=q.useRef(o??!1),C=q.useRef(f??!1),[T,S]=q.useState(void 0),L=q.useCallback((R=b.current)=>{if(!m.current)return;let P=m.current;const w=R?g.current:v.current;if(!w||!P)return;const F=C.current;for(;getComputedStyle(P).display==="contents";){const k=P.parentElement;if(!k)return;P=k}const E=P.getBoundingClientRect(),O=w.getBoundingClientRect();if(F){const k=O.top-E.top+O.height/2,U=Math.max(k*2.4,E.width*1.2,Math.min(w.clientWidth,w.clientHeight));return{width:U,height:U,left:O.x-E.x+(O.width-U)/2,top:O.y-E.y+(O.height-U)/2}}const I=Math.min(w.clientWidth,w.clientHeight);return{width:I,height:I,left:O.x-E.x+(O.width-I)/2,top:O.y-E.y+(O.height-I)/2}},[]);return q.useLayoutEffect(()=>{b.current=o??!1,C.current=f??!1,S(L(b.current))},[o,f,L]),q.useLayoutEffect(()=>{const R=g.current,P=v.current;if(!R||!P)return;const w=new ResizeObserver(()=>{S(L(b.current))});return w.observe(R),w.observe(P),S(L(b.current)),()=>{w.disconnect()}},[L]),Y.jsxs("div",{className:on(u,!c&&qn.verticalLayout,!c&&o&&qn.hideLyric),ref:m,...d,children:[Y.jsx("div",{className:qn.thumb,children:n}),Y.jsxs("div",{className:qn.lyricLayout,children:[Y.jsx("div",{className:qn.phonySmallCover,ref:v}),Y.jsx("div",{className:qn.smallControls,children:e}),Y.jsx("div",{className:qn.lyric,children:a})]}),Y.jsxs("div",{className:qn.noLyricLayout,children:[Y.jsx("div",{className:qn.phonyBigCover,ref:g}),Y.jsx("div",{className:qn.bigControls,children:i})]}),T&&Y.jsx(Kn.div,{className:on(qn.coverFrame,f&&qn.immerseCover),animate:T,initial:!1,transition:{type:"spring",stiffness:200,damping:30},ref:x,children:t})]})};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const aI=n=>{const t=Re.c(40);let e,i,a,o,c,u,f,d,m,g,v,x,b;t[0]!==n?({thumbSlot:x,controlsSlot:a,horizontalBottomControls:u,smallControlsSlot:v,bigControlsSlot:i,coverSlot:o,lyricSlot:f,backgroundSlot:e,hideLyric:c,verticalImmerseCover:b,onLayoutChange:m,onElementMounted:d,...g}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o,t[5]=c,t[6]=u,t[7]=f,t[8]=d,t[9]=m,t[10]=g,t[11]=v,t[12]=x,t[13]=b):(e=t[1],i=t[2],a=t[3],o=t[4],c=t[5],u=t[6],f=t[7],d=t[8],m=t[9],g=t[10],v=t[11],x=t[12],b=t[13]);const[C,T]=q.useState(!1),S=q.useRef(null);let L;t[14]!==d?(L=U=>{S.current=U,d&&d(U)},t[14]=d,t[15]=L):L=t[15];const R=L;let P,w;t[16]===Symbol.for("react.memo_cache_sentinel")?(P=()=>{const U=S.current;if(!U)return;T(U.clientWidth{const J=U.getBoundingClientRect();T(J.widthG.disconnect()},w=[],t[16]=P,t[17]=w):(P=t[16],w=t[17]),q.useLayoutEffect(P,w);let F,E;t[18]!==C||t[19]!==m?(F=()=>{m?.(C)},E=[C,m],t[18]=C,t[19]=m,t[20]=F,t[21]=E):(F=t[20],E=t[21]),q.useLayoutEffect(F,E);let O;t[22]!==e?(O=Y.jsx("div",{className:iI.background,children:e}),t[22]=e,t[23]=O):O=t[23];let I;t[24]!==i||t[25]!==a||t[26]!==o||t[27]!==c||t[28]!==u||t[29]!==C||t[30]!==f||t[31]!==v||t[32]!==x||t[33]!==b?(I=C?Y.jsx(rI,{thumbSlot:x,smallControlsSlot:v,bigControlsSlot:i,coverSlot:o,lyricSlot:f,hideLyric:c,immerseCover:b}):Y.jsx(sI,{thumbSlot:x,controlsSlot:a,coverSlot:o,lyricSlot:f,bottomControls:u,hideLyric:c}),t[24]=i,t[25]=a,t[26]=o,t[27]=c,t[28]=u,t[29]=C,t[30]=f,t[31]=v,t[32]=x,t[33]=b,t[34]=I):I=t[34];let k;return t[35]!==g||t[36]!==R||t[37]!==O||t[38]!==I?(k=Y.jsxs("div",{...g,ref:R,children:[O,I]}),t[35]=g,t[36]=R,t[37]=O,t[38]=I,t[39]=k):k=t[39],k};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};function ub(n){const t=n<0,e=Math.abs(n|0),i=e%60,a=Math.floor((e-i)/60),o="0".repeat(2-i.toString().length)+i;return`${t?"-":""}${a}:${o}`}const oI=(n,t)=>Y.jsx("svg",{width:64,height:64,viewBox:"0 0 64 64",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M11.8633 31.8984C11.8633 29.138 12.3841 26.5469 13.4258 24.125C14.4674 21.6901 15.9128 19.5482 17.7617 17.6992C19.6107 15.8372 21.7526 14.3854 24.1875 13.3438C26.6224 12.2891 29.2266 11.7617 32 11.7617C34.7734 11.7617 37.3776 12.2891 39.8125 13.3438C42.2474 14.3854 44.3893 15.8372 46.2383 17.6992C48.0872 19.5482 49.5326 21.6901 50.5742 24.125C51.6159 26.5469 52.1367 29.138 52.1367 31.8984C52.1367 34.6719 51.6029 37.2826 50.5352 39.7305C49.4674 42.1784 48.0156 44.3073 46.1797 46.1172C46.0365 46.2604 45.8802 46.332 45.7109 46.332C45.5547 46.332 45.4049 46.2539 45.2617 46.0977L44.3438 45.0625C44.0964 44.763 44.1094 44.4635 44.3828 44.1641C45.9453 42.6016 47.1823 40.7656 48.0938 38.6562C49.0182 36.5339 49.4805 34.2812 49.4805 31.8984C49.4805 29.5026 49.0247 27.2565 48.1133 25.1602C47.2018 23.0508 45.9388 21.1953 44.3242 19.5938C42.7227 17.9792 40.8672 16.7161 38.7578 15.8047C36.6484 14.8932 34.3958 14.4375 32 14.4375C29.6042 14.4375 27.3516 14.8932 25.2422 15.8047C23.1328 16.7161 21.2708 17.9792 19.6562 19.5938C18.0547 21.1953 16.7982 23.0508 15.8867 25.1602C14.9753 27.2565 14.5195 29.5026 14.5195 31.8984C14.5195 34.2812 14.9753 36.5273 15.8867 38.6367C16.7982 40.7461 18.0417 42.5885 19.6172 44.1641C19.8776 44.4635 19.8841 44.7565 19.6367 45.043L18.7188 46.0781C18.5885 46.2344 18.4323 46.3125 18.25 46.3125C18.0807 46.3125 17.9245 46.2409 17.7812 46.0977C15.9583 44.2878 14.513 42.1654 13.4453 39.7305C12.3906 37.2826 11.8633 34.6719 11.8633 31.8984ZM17.5469 31.8984C17.5469 29.9193 17.918 28.0573 18.6602 26.3125C19.4154 24.5677 20.457 23.0312 21.7852 21.7031C23.1133 20.375 24.6497 19.3333 26.3945 18.5781C28.1393 17.8229 30.0078 17.4453 32 17.4453C33.9792 17.4453 35.8411 17.8229 37.5859 18.5781C39.3438 19.3333 40.8867 20.375 42.2148 21.7031C43.543 23.0312 44.5781 24.5677 45.3203 26.3125C46.0755 28.0573 46.4531 29.9193 46.4531 31.8984C46.4531 33.8255 46.0885 35.6419 45.3594 37.3477C44.6432 39.0404 43.6667 40.5312 42.4297 41.8203C42.2865 41.9766 42.1237 42.0547 41.9414 42.0547C41.7721 42.0547 41.6224 41.9766 41.4922 41.8203L40.5547 40.7852C40.3073 40.5117 40.3073 40.2122 40.5547 39.8867C41.5573 38.8581 42.3451 37.6602 42.918 36.293C43.4909 34.9128 43.7773 33.4479 43.7773 31.8984C43.7773 30.2839 43.4714 28.7669 42.8594 27.3477C42.2474 25.9284 41.3945 24.6784 40.3008 23.5977C39.2201 22.5169 37.9701 21.6706 36.5508 21.0586C35.1315 20.4336 33.6146 20.1211 32 20.1211C30.3724 20.1211 28.849 20.4336 27.4297 21.0586C26.0104 21.6706 24.7604 22.5169 23.6797 23.5977C22.599 24.6784 21.7526 25.9284 21.1406 27.3477C20.5286 28.7669 20.2227 30.2839 20.2227 31.8984C20.2227 33.4349 20.5026 34.8932 21.0625 36.2734C21.6354 37.6536 22.4232 38.8581 23.4258 39.8867C23.6732 40.2122 23.6732 40.5117 23.4258 40.7852L22.5078 41.8008C22.3776 41.957 22.2214 42.0417 22.0391 42.0547C21.8568 42.0547 21.694 41.9701 21.5508 41.8008C20.3138 40.5117 19.3372 39.0208 18.6211 37.3281C17.9049 35.6224 17.5469 33.8125 17.5469 31.8984ZM23.2305 31.8984C23.2305 30.2969 23.6276 28.832 24.4219 27.5039C25.2161 26.1758 26.2708 25.1146 27.5859 24.3203C28.9141 23.526 30.3854 23.1289 32 23.1289C33.6146 23.1289 35.0794 23.526 36.3945 24.3203C37.7227 25.1146 38.7839 26.1758 39.5781 27.5039C40.3724 28.832 40.7695 30.2969 40.7695 31.8984C40.7695 32.9661 40.5807 33.9753 40.2031 34.9258C39.8255 35.8633 39.3047 36.7031 38.6406 37.4453C38.5104 37.6276 38.3542 37.7188 38.1719 37.7188C38.0026 37.7188 37.8398 37.6406 37.6836 37.4844L36.7266 36.4688C36.4792 36.2214 36.4596 35.9414 36.668 35.6289C37.1237 35.1341 37.4753 34.5677 37.7227 33.9297C37.9701 33.2917 38.0938 32.6146 38.0938 31.8984C38.0938 30.7917 37.8138 29.776 37.2539 28.8516C36.707 27.9271 35.9714 27.1914 35.0469 26.6445C34.1224 26.0846 33.1068 25.8047 32 25.8047C30.8932 25.8047 29.8776 26.0846 28.9531 26.6445C28.0286 27.1914 27.2865 27.9271 26.7266 28.8516C26.1797 29.776 25.9062 30.7917 25.9062 31.8984C25.9062 32.6016 26.0299 33.2721 26.2773 33.9102C26.5247 34.5482 26.8698 35.1146 27.3125 35.6094C27.5078 35.9219 27.4883 36.2018 27.2539 36.4492L26.2969 37.4648C26.1406 37.6211 25.9714 37.6992 25.7891 37.6992C25.6198 37.6992 25.4701 37.6146 25.3398 37.4453C24.6758 36.7031 24.1549 35.8568 23.7773 34.9062C23.4128 33.9557 23.2305 32.9531 23.2305 31.8984ZM19.8906 51.3711C19.4089 51.3711 19.0898 51.1758 18.9336 50.7852C18.7773 50.3945 18.8555 50.0299 19.168 49.6914L31.1016 36.1758C31.3359 35.9023 31.6289 35.7656 31.9805 35.7656C32.332 35.7656 32.625 35.9023 32.8594 36.1758L44.8125 49.6914C45.112 50.0299 45.1836 50.3945 45.0273 50.7852C44.8711 51.1758 44.5586 51.3711 44.0898 51.3711H19.8906Z",fill:"currentColor"})}),L8=q.forwardRef(oI);var lI={toggleIconButton:"_6-mjea_toggleIconButton"};const cI=(n,t)=>Y.jsx("svg",{width:64,height:64,viewBox:"0 0 64 64",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M22.8594 53.9102C22 53.9102 21.3229 53.6302 20.8281 53.0703C20.3464 52.5104 20.1055 51.7617 20.1055 50.8242V46.1953H18.9727C17.1237 46.1953 15.5156 45.8242 14.1484 45.082C12.7812 44.3398 11.7201 43.2721 10.9648 41.8789C10.2227 40.4857 9.85156 38.8125 9.85156 36.8594V21.5664C9.85156 19.6133 10.2161 17.9401 10.9453 16.5469C11.6875 15.1536 12.7552 14.0859 14.1484 13.3438C15.5417 12.5885 17.2214 12.2109 19.1875 12.2109H44.793C46.7721 12.2109 48.4518 12.5885 49.832 13.3438C51.2253 14.0859 52.2865 15.1536 53.0156 16.5469C53.7578 17.9401 54.1289 19.6133 54.1289 21.5664V36.8594C54.1289 38.8125 53.7578 40.4857 53.0156 41.8789C52.2865 43.2721 51.2253 44.3398 49.832 45.082C48.4518 45.8242 46.7721 46.1953 44.793 46.1953H33.0742L26.4336 52.0547C25.7044 52.6927 25.0729 53.1615 24.5391 53.4609C24.0182 53.7604 23.4583 53.9102 22.8594 53.9102ZM23.9141 49.0469L30.0859 42.9727C30.5156 42.5299 30.9193 42.237 31.2969 42.0938C31.6745 41.9375 32.1758 41.8594 32.8008 41.8594H44.5977C46.3424 41.8594 47.6445 41.4232 48.5039 40.5508C49.3633 39.6784 49.793 38.3828 49.793 36.6641V21.7422C49.793 20.0365 49.3633 18.7474 48.5039 17.875C47.6445 17.0026 46.3424 16.5664 44.5977 16.5664H19.3828C17.625 16.5664 16.3164 17.0026 15.457 17.875C14.6107 18.7474 14.1875 20.0365 14.1875 21.7422V36.6641C14.1875 38.3828 14.6107 39.6784 15.457 40.5508C16.3164 41.4232 17.625 41.8594 19.3828 41.8594H22.2344C22.8073 41.8594 23.2305 41.9896 23.5039 42.25C23.7773 42.4974 23.9141 42.9271 23.9141 43.5391V49.0469ZM22.4492 27.1914C22.4492 25.9935 22.8529 25.0104 23.6602 24.2422C24.4674 23.474 25.4701 23.0898 26.668 23.0898C28.0221 23.0898 29.1029 23.5716 29.9102 24.5352C30.7305 25.4857 31.1406 26.6576 31.1406 28.0508C31.1406 29.2096 30.9323 30.2383 30.5156 31.1367C30.112 32.0221 29.5911 32.7708 28.9531 33.3828C28.3281 33.9948 27.6771 34.457 27 34.7695C26.3229 35.082 25.7174 35.2383 25.1836 35.2383C24.8841 35.2383 24.6367 35.1536 24.4414 34.9844C24.2461 34.8151 24.1484 34.5938 24.1484 34.3203C24.1484 34.0859 24.2135 33.8906 24.3438 33.7344C24.487 33.5651 24.7214 33.4414 25.0469 33.3633C25.5677 33.2331 26.0625 33.0312 26.5312 32.7578C27.013 32.4714 27.4362 32.1328 27.8008 31.7422C28.1654 31.3385 28.4453 30.8828 28.6406 30.375H28.3867C28.1263 30.7005 27.7943 30.9284 27.3906 31.0586C26.987 31.1758 26.5573 31.2344 26.1016 31.2344C25.0078 31.2344 24.1224 30.8503 23.4453 30.082C22.7812 29.3008 22.4492 28.3372 22.4492 27.1914ZM33.0742 27.1914C33.0742 25.9935 33.4714 25.0104 34.2656 24.2422C35.0729 23.474 36.082 23.0898 37.293 23.0898C38.6471 23.0898 39.7279 23.5716 40.5352 24.5352C41.3555 25.4857 41.7656 26.6576 41.7656 28.0508C41.7656 29.2096 41.5573 30.2383 41.1406 31.1367C40.737 32.0221 40.2161 32.7708 39.5781 33.3828C38.9531 33.9948 38.2956 34.457 37.6055 34.7695C36.9284 35.082 36.3294 35.2383 35.8086 35.2383C35.5091 35.2383 35.2617 35.1536 35.0664 34.9844C34.8711 34.8151 34.7734 34.5938 34.7734 34.3203C34.7734 34.0859 34.8385 33.8906 34.9688 33.7344C35.112 33.5651 35.3529 33.4414 35.6914 33.3633C36.1992 33.2331 36.6875 33.0312 37.1562 32.7578C37.638 32.4714 38.0612 32.1328 38.4258 31.7422C38.7904 31.3385 39.0703 30.8828 39.2656 30.375H39.0117C38.7513 30.7005 38.4193 30.9284 38.0156 31.0586C37.612 31.1758 37.1823 31.2344 36.7266 31.2344C35.6328 31.2344 34.7474 30.8503 34.0703 30.082C33.4062 29.3008 33.0742 28.3372 33.0742 27.1914Z",fill:"currentColor"})}),uI=q.forwardRef(cI),hI=(n,t)=>Y.jsxs("svg",{width:64,height:64,viewBox:"0 0 64 64",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:[Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1.91256 7.67068C0 11.0858 0 15.6405 0 24.75V39.25C0 48.3595 0 52.9142 1.91256 56.3293C3.26425 58.7429 5.25707 60.7357 7.67068 62.0874C11.0858 64 15.6405 64 24.75 64H39.25C48.3595 64 52.9142 64 56.3293 62.0874C58.7429 60.7357 60.7357 58.7429 62.0874 56.3293C64 52.9142 64 48.3595 64 39.25V24.75C64 15.6405 64 11.0858 62.0874 7.67068C60.7357 5.25707 58.7429 3.26425 56.3293 1.91256C52.9142 0 48.3595 0 39.25 0H24.75C15.6405 0 11.0858 0 7.67068 1.91256C5.25707 3.26425 3.26425 5.25707 1.91256 7.67068ZM20.8281 53.0703C21.3229 53.6302 22 53.9102 22.8594 53.9102C23.4583 53.9102 24.0182 53.7604 24.5391 53.4609C25.0729 53.1615 25.7044 52.6927 26.4336 52.0547L33.0742 46.1953H44.793C46.7721 46.1953 48.4518 45.8242 49.832 45.082C51.2253 44.3398 52.2865 43.2721 53.0156 41.8789C53.7578 40.4857 54.1289 38.8125 54.1289 36.8594V21.5664C54.1289 19.6133 53.7578 17.9401 53.0156 16.5469C52.2865 15.1536 51.2253 14.0859 49.832 13.3438C48.4518 12.5885 46.7721 12.2109 44.793 12.2109H19.1875C17.2214 12.2109 15.5417 12.5885 14.1484 13.3438C12.7552 14.0859 11.6875 15.1536 10.9453 16.5469C10.2161 17.9401 9.85156 19.6133 9.85156 21.5664V36.8594C9.85156 38.8125 10.2227 40.4857 10.9648 41.8789C11.7201 43.2721 12.7812 44.3398 14.1484 45.082C15.5156 45.8242 17.1237 46.1953 18.9727 46.1953H20.1055V50.8242C20.1055 51.7617 20.3464 52.5104 20.8281 53.0703Z",fill:"currentColor"}),Y.jsx("path",{d:"M22.4492 27.1914C22.4492 25.9935 22.8529 25.0104 23.6602 24.2422C24.4674 23.474 25.4701 23.0898 26.668 23.0898C28.0221 23.0898 29.1029 23.5716 29.9102 24.5352C30.7305 25.4857 31.1406 26.6576 31.1406 28.0508C31.1406 29.2096 30.9323 30.2383 30.5156 31.1367C30.112 32.0221 29.5911 32.7708 28.9531 33.3828C28.3281 33.9948 27.6771 34.457 27 34.7695C26.3229 35.082 25.7174 35.2383 25.1836 35.2383C24.8841 35.2383 24.6367 35.1536 24.4414 34.9844C24.2461 34.8151 24.1484 34.5938 24.1484 34.3203C24.1484 34.0859 24.2135 33.8906 24.3438 33.7344C24.487 33.5651 24.7214 33.4414 25.0469 33.3633C25.5677 33.2331 26.0625 33.0312 26.5312 32.7578C27.013 32.4714 27.4362 32.1328 27.8008 31.7422C28.1654 31.3385 28.4453 30.8828 28.6406 30.375H28.3867C28.1263 30.7005 27.7943 30.9284 27.3906 31.0586C26.987 31.1758 26.5573 31.2344 26.1016 31.2344C25.0078 31.2344 24.1224 30.8503 23.4453 30.082C22.7812 29.3008 22.4492 28.3372 22.4492 27.1914ZM33.0742 27.1914C33.0742 25.9935 33.4714 25.0104 34.2656 24.2422C35.0729 23.474 36.082 23.0898 37.293 23.0898C38.6471 23.0898 39.7279 23.5716 40.5352 24.5352C41.3555 25.4857 41.7656 26.6576 41.7656 28.0508C41.7656 29.2096 41.5573 30.2383 41.1406 31.1367C40.737 32.0221 40.2161 32.7708 39.5781 33.3828C38.9531 33.9948 38.2956 34.457 37.6055 34.7695C36.9284 35.082 36.3294 35.2383 35.8086 35.2383C35.5091 35.2383 35.2617 35.1536 35.0664 34.9844C34.8711 34.8151 34.7734 34.5938 34.7734 34.3203C34.7734 34.0859 34.8385 33.8906 34.9688 33.7344C35.112 33.5651 35.3529 33.4414 35.6914 33.3633C36.1992 33.2331 36.6875 33.0312 37.1562 32.7578C37.638 32.4714 38.0612 32.1328 38.4258 31.7422C38.7904 31.3385 39.0703 30.8828 39.2656 30.375H39.0117C38.7513 30.7005 38.4193 30.9284 38.0156 31.0586C37.612 31.1758 37.1823 31.2344 36.7266 31.2344C35.6328 31.2344 34.7474 30.8503 34.0703 30.082C33.4062 29.3008 33.0742 28.3372 33.0742 27.1914Z",fill:"currentColor"})]}),fI=q.forwardRef(hI),dI=(n,t)=>Y.jsx("svg",{width:64,height:64,viewBox:"0 0 64 64",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M23.9922 21.8594C23.4062 21.8594 22.9115 21.6641 22.5078 21.2734C22.1172 20.8698 21.9219 20.375 21.9219 19.7891C21.9219 19.2161 22.1172 18.7279 22.5078 18.3242C22.9115 17.9206 23.4062 17.7188 23.9922 17.7188H50.418C50.9909 17.7188 51.4792 17.9206 51.8828 18.3242C52.2865 18.7279 52.4883 19.2161 52.4883 19.7891C52.4883 20.375 52.2865 20.8698 51.8828 21.2734C51.4792 21.6641 50.9909 21.8594 50.418 21.8594H23.9922ZM23.9922 33.9883C23.4062 33.9883 22.9115 33.7865 22.5078 33.3828C22.1172 32.9792 21.9219 32.4909 21.9219 31.918C21.9219 31.3451 22.1172 30.8633 22.5078 30.4727C22.9115 30.069 23.4062 29.8672 23.9922 29.8672H50.418C50.9909 29.8672 51.4792 30.069 51.8828 30.4727C52.2865 30.8633 52.4883 31.3451 52.4883 31.918C52.4883 32.5039 52.2865 32.9987 51.8828 33.4023C51.4792 33.793 50.9909 33.9883 50.418 33.9883H23.9922ZM23.9922 46.1172C23.4062 46.1172 22.9115 45.9219 22.5078 45.5312C22.1172 45.1276 21.9219 44.6328 21.9219 44.0469C21.9219 43.474 22.1172 42.9857 22.5078 42.582C22.9115 42.1784 23.4062 41.9766 23.9922 41.9766H50.418C50.9909 41.9766 51.4792 42.1784 51.8828 42.582C52.2865 42.9857 52.4883 43.474 52.4883 44.0469C52.4883 44.6328 52.2865 45.1276 51.8828 45.5312C51.4792 45.9219 50.9909 46.1172 50.418 46.1172H23.9922ZM14.4805 22.7383C13.6602 22.7383 12.957 22.4518 12.3711 21.8789C11.7982 21.306 11.5117 20.6094 11.5117 19.7891C11.5117 18.9688 11.7982 18.2721 12.3711 17.6992C12.957 17.1263 13.6602 16.8398 14.4805 16.8398C15.2878 16.8398 15.9844 17.1263 16.5703 17.6992C17.1562 18.2721 17.4492 18.9688 17.4492 19.7891C17.4492 20.6094 17.1562 21.306 16.5703 21.8789C15.9844 22.4518 15.2878 22.7383 14.4805 22.7383ZM14.4805 34.8867C13.6602 34.8867 12.957 34.5938 12.3711 34.0078C11.7982 33.4219 11.5117 32.7253 11.5117 31.918C11.5117 31.1107 11.7982 30.4141 12.3711 29.8281C12.957 29.2422 13.6602 28.9492 14.4805 28.9492C15.2878 28.9492 15.9844 29.2422 16.5703 29.8281C17.1562 30.4141 17.4492 31.1107 17.4492 31.918C17.4492 32.7253 17.1562 33.4219 16.5703 34.0078C15.9844 34.5938 15.2878 34.8867 14.4805 34.8867ZM14.4805 47.0156C13.6602 47.0156 12.957 46.7227 12.3711 46.1367C11.7982 45.5638 11.5117 44.8672 11.5117 44.0469C11.5117 43.2266 11.7982 42.5299 12.3711 41.957C12.957 41.3841 13.6602 41.0977 14.4805 41.0977C15.2878 41.0977 15.9844 41.3841 16.5703 41.957C17.1562 42.5299 17.4492 43.2266 17.4492 44.0469C17.4492 44.8672 17.1562 45.5638 16.5703 46.1367C15.9844 46.7227 15.2878 47.0156 14.4805 47.0156Z",fill:"currentColor"})}),mI=q.forwardRef(dI),pI=(n,t)=>Y.jsx("svg",{width:64,height:64,viewBox:"0 0 64 64",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1.91256 7.67068C0 11.0858 0 15.6405 0 24.75V39.25C0 48.3595 0 52.9142 1.91256 56.3293C3.26425 58.7429 5.25707 60.7357 7.67068 62.0874C11.0858 64 15.6405 64 24.75 64H39.25C48.3595 64 52.9142 64 56.3293 62.0874C58.7429 60.7357 60.7357 58.7429 62.0874 56.3293C64 52.9142 64 48.3595 64 39.25V24.75C64 15.6405 64 11.0858 62.0874 7.67068C60.7357 5.25706 58.7429 3.26425 56.3293 1.91256C52.9142 0 48.3595 0 39.25 0H24.75C15.6405 0 11.0858 0 7.67068 1.91256C5.25707 3.26425 3.26425 5.25706 1.91256 7.67068ZM22.5078 21.2734C22.9115 21.6641 23.4062 21.8594 23.9922 21.8594H50.418C50.9909 21.8594 51.4792 21.6641 51.8828 21.2734C52.2865 20.8698 52.4883 20.375 52.4883 19.7891C52.4883 19.2161 52.2865 18.7279 51.8828 18.3242C51.4792 17.9206 50.9909 17.7188 50.418 17.7188H23.9922C23.4062 17.7188 22.9115 17.9206 22.5078 18.3242C22.1172 18.7279 21.9219 19.2161 21.9219 19.7891C21.9219 20.375 22.1172 20.8698 22.5078 21.2734ZM22.5078 33.3828C22.9115 33.7865 23.4062 33.9883 23.9922 33.9883H50.418C50.9909 33.9883 51.4792 33.793 51.8828 33.4023C52.2865 32.9987 52.4883 32.5039 52.4883 31.918C52.4883 31.3451 52.2865 30.8633 51.8828 30.4727C51.4792 30.069 50.9909 29.8672 50.418 29.8672H23.9922C23.4062 29.8672 22.9115 30.069 22.5078 30.4727C22.1172 30.8633 21.9219 31.3451 21.9219 31.918C21.9219 32.4909 22.1172 32.9792 22.5078 33.3828ZM22.5078 45.5312C22.9115 45.9219 23.4062 46.1172 23.9922 46.1172H50.418C50.9909 46.1172 51.4792 45.9219 51.8828 45.5312C52.2865 45.1276 52.4883 44.6328 52.4883 44.0469C52.4883 43.474 52.2865 42.9857 51.8828 42.582C51.4792 42.1784 50.9909 41.9766 50.418 41.9766H23.9922C23.4062 41.9766 22.9115 42.1784 22.5078 42.582C22.1172 42.9857 21.9219 43.474 21.9219 44.0469C21.9219 44.6328 22.1172 45.1276 22.5078 45.5312ZM12.3711 21.8789C12.957 22.4518 13.6602 22.7383 14.4805 22.7383C15.2878 22.7383 15.9844 22.4518 16.5703 21.8789C17.1562 21.306 17.4492 20.6094 17.4492 19.7891C17.4492 18.9688 17.1562 18.2721 16.5703 17.6992C15.9844 17.1263 15.2878 16.8398 14.4805 16.8398C13.6602 16.8398 12.957 17.1263 12.3711 17.6992C11.7982 18.2721 11.5117 18.9688 11.5117 19.7891C11.5117 20.6094 11.7982 21.306 12.3711 21.8789ZM12.3711 34.0078C12.957 34.5938 13.6602 34.8867 14.4805 34.8867C15.2878 34.8867 15.9844 34.5938 16.5703 34.0078C17.1562 33.4219 17.4492 32.7253 17.4492 31.918C17.4492 31.1107 17.1562 30.4141 16.5703 29.8281C15.9844 29.2422 15.2878 28.9492 14.4805 28.9492C13.6602 28.9492 12.957 29.2422 12.3711 29.8281C11.7982 30.4141 11.5117 31.1107 11.5117 31.918C11.5117 32.7253 11.7982 33.4219 12.3711 34.0078ZM12.3711 46.1367C12.957 46.7227 13.6602 47.0156 14.4805 47.0156C15.2878 47.0156 15.9844 46.7227 16.5703 46.1367C17.1562 45.5638 17.4492 44.8672 17.4492 44.0469C17.4492 43.2266 17.1562 42.5299 16.5703 41.957C15.9844 41.3841 15.2878 41.0977 14.4805 41.0977C13.6602 41.0977 12.957 41.3841 12.3711 41.957C11.7982 42.5299 11.5117 43.2266 11.5117 44.0469C11.5117 44.8672 11.7982 45.5638 12.3711 46.1367Z",fill:"currentColor"})}),gI=q.forwardRef(pI);globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const yI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M13.0293 28.9805C12.4727 28.9805 12.0039 28.79 11.623 28.4092C11.2422 28.0186 11.0518 27.5498 11.0518 27.0029V25.3623C11.0518 23.7998 11.4033 22.457 12.1064 21.334C12.8096 20.2109 13.8057 19.3516 15.0947 18.7559C16.3838 18.1504 17.9219 17.8477 19.709 17.8477H30.6367V14.7422C30.6367 14.2832 30.7637 13.9268 31.0176 13.6729C31.2715 13.4092 31.623 13.2773 32.0723 13.2773C32.2871 13.2773 32.4775 13.3115 32.6436 13.3799C32.8193 13.4482 32.9805 13.541 33.127 13.6582L39.2354 18.8145C39.5869 19.0977 39.7578 19.4443 39.748 19.8545C39.748 20.2549 39.5771 20.5967 39.2354 20.8799L33.127 26.0361C32.9805 26.1533 32.8193 26.2461 32.6436 26.3145C32.4775 26.3828 32.2871 26.417 32.0723 26.417C31.623 26.417 31.2715 26.29 31.0176 26.0361C30.7637 25.7725 30.6367 25.416 30.6367 24.9668V21.6855H19.4893C18.1025 21.6855 17.0039 22.0615 16.1934 22.8135C15.3926 23.5557 14.9922 24.5713 14.9922 25.8604V27.0029C14.9922 27.5498 14.8018 28.0186 14.4209 28.4092C14.04 28.79 13.5762 28.9805 13.0293 28.9805ZM42.9707 26.959C43.5176 26.959 43.9814 27.1494 44.3623 27.5303C44.7529 27.9111 44.9482 28.3799 44.9482 28.9365V30.5771C44.9482 32.1299 44.5967 33.4727 43.8936 34.6055C43.1904 35.7285 42.1895 36.5928 40.8906 37.1982C39.6016 37.7939 38.0684 38.0918 36.291 38.0918H25.3633V41.1973C25.3633 41.6465 25.2363 41.998 24.9824 42.252C24.7285 42.5156 24.377 42.6475 23.9277 42.6475C23.7129 42.6475 23.5176 42.6133 23.3418 42.5449C23.166 42.4766 23.0098 42.3838 22.873 42.2666L16.75 37.1104C16.418 36.8271 16.252 36.4854 16.252 36.085C16.252 35.6846 16.418 35.3379 16.75 35.0449L22.873 29.8887C23.0098 29.7715 23.166 29.6787 23.3418 29.6104C23.5176 29.542 23.7129 29.5078 23.9277 29.5078C24.377 29.5078 24.7285 29.6396 24.9824 29.9033C25.2363 30.1572 25.3633 30.5137 25.3633 30.9727V34.2393H36.5107C37.8975 34.2393 38.9912 33.8682 39.792 33.126C40.6025 32.3838 41.0078 31.3682 41.0078 30.0791V28.9365C41.0078 28.3799 41.1934 27.9111 41.5645 27.5303C41.9453 27.1494 42.4141 26.959 42.9707 26.959Z",fill:"currentColor"})}),vI=q.forwardRef(yI),bI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1.65755 6.64792C0 9.6077 0 13.5551 0 21.45V34.55C0 42.4449 0 46.3923 1.65755 49.3521C2.82902 51.4439 4.55612 53.171 6.64792 54.3424C9.6077 56 13.5551 56 21.45 56H34.55C42.4449 56 46.3923 56 49.3521 54.3424C51.4439 53.171 53.171 51.4439 54.3424 49.3521C56 46.3923 56 42.4449 56 34.55V21.45C56 13.5551 56 9.6077 54.3424 6.64792C53.171 4.55612 51.4439 2.82902 49.3521 1.65755C46.3923 0 42.4449 0 34.55 0H21.45C13.5551 0 9.6077 0 6.64792 1.65755C4.55612 2.82902 2.82902 4.55612 1.65755 6.64792ZM11.623 28.4092C12.0039 28.79 12.4727 28.9805 13.0293 28.9805C13.5762 28.9805 14.04 28.79 14.4209 28.4092C14.8018 28.0186 14.9922 27.5498 14.9922 27.0029V25.8604C14.9922 24.5713 15.3926 23.5557 16.1934 22.8135C17.0039 22.0615 18.1025 21.6855 19.4893 21.6855H30.6367V24.9668C30.6367 25.416 30.7637 25.7725 31.0176 26.0361C31.2715 26.29 31.623 26.417 32.0723 26.417C32.2871 26.417 32.4775 26.3828 32.6436 26.3145C32.8193 26.2461 32.9805 26.1533 33.127 26.0361L39.2354 20.8799C39.5771 20.5967 39.748 20.2549 39.748 19.8545C39.7578 19.4443 39.5869 19.0977 39.2354 18.8145L33.127 13.6582C32.9805 13.541 32.8193 13.4482 32.6436 13.3799C32.4775 13.3115 32.2871 13.2773 32.0723 13.2773C31.623 13.2773 31.2715 13.4092 31.0176 13.6729C30.7637 13.9268 30.6367 14.2832 30.6367 14.7422V17.8477H19.709C17.9219 17.8477 16.3838 18.1504 15.0947 18.7559C13.8057 19.3516 12.8096 20.2109 12.1064 21.334C11.4033 22.457 11.0518 23.7998 11.0518 25.3623V27.0029C11.0518 27.5498 11.2422 28.0186 11.623 28.4092ZM44.3623 27.5303C43.9814 27.1494 43.5176 26.959 42.9707 26.959C42.4141 26.959 41.9453 27.1494 41.5645 27.5303C41.1934 27.9111 41.0078 28.3799 41.0078 28.9365V30.0791C41.0078 31.3682 40.6025 32.3838 39.792 33.126C38.9912 33.8682 37.8975 34.2393 36.5107 34.2393H25.3633V30.9727C25.3633 30.5137 25.2363 30.1572 24.9824 29.9033C24.7285 29.6396 24.377 29.5078 23.9277 29.5078C23.7129 29.5078 23.5176 29.542 23.3418 29.6104C23.166 29.6787 23.0098 29.7715 22.873 29.8887L16.75 35.0449C16.418 35.3379 16.252 35.6846 16.252 36.085C16.252 36.4854 16.418 36.8271 16.75 37.1104L22.873 42.2666C23.0098 42.3838 23.166 42.4766 23.3418 42.5449C23.5176 42.6133 23.7129 42.6475 23.9277 42.6475C24.377 42.6475 24.7285 42.5156 24.9824 42.252C25.2363 41.998 25.3633 41.6465 25.3633 41.1973V38.0918H36.291C38.0684 38.0918 39.6016 37.7939 40.8906 37.1982C42.1895 36.5928 43.1904 35.7285 43.8936 34.6055C44.5967 33.4727 44.9482 32.1299 44.9482 30.5771V28.9365C44.9482 28.3799 44.7529 27.9111 44.3623 27.5303Z",fill:"currentColor"})}),xI=q.forwardRef(bI),TI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M9.55762 36.7148C9.55762 36.1777 9.74316 35.7285 10.1143 35.3672C10.4951 35.0059 10.9639 34.8252 11.5205 34.8252H14.7285C15.6562 34.8252 16.4375 34.6592 17.0723 34.3271C17.707 33.9951 18.3369 33.4434 18.9619 32.6719L27.7656 21.8613C28.8008 20.5918 29.8457 19.7275 30.9004 19.2686C31.9551 18.7998 33.2051 18.5654 34.6504 18.5654H37.3311V15.1963C37.3311 14.7471 37.458 14.3955 37.7119 14.1416C37.9658 13.8779 38.3174 13.7461 38.7666 13.7461C38.9814 13.7461 39.1719 13.7803 39.3379 13.8486C39.5137 13.917 39.6748 14.0098 39.8213 14.127L45.9297 19.2832C46.2715 19.5664 46.4424 19.9131 46.4424 20.3232C46.4424 20.7236 46.2715 21.0654 45.9297 21.3486L39.8213 26.5049C39.6748 26.6221 39.5137 26.7148 39.3379 26.7832C39.1719 26.8516 38.9814 26.8857 38.7666 26.8857C38.3174 26.8857 37.9658 26.7588 37.7119 26.5049C37.458 26.2412 37.3311 25.8848 37.3311 25.4355V22.3447H34.4746C33.8496 22.3447 33.3076 22.4131 32.8486 22.5498C32.3896 22.6768 31.96 22.8965 31.5596 23.209C31.1689 23.5215 30.7588 23.9414 30.3291 24.4688L21.1006 35.748C20.2607 36.7539 19.3916 37.4814 18.4932 37.9307C17.5947 38.3701 16.4766 38.5898 15.1387 38.5898H11.5205C10.9639 38.5898 10.4951 38.4092 10.1143 38.0479C9.74316 37.6865 9.55762 37.2422 9.55762 36.7148ZM9.55762 20.4551C9.55762 19.918 9.74316 19.4688 10.1143 19.1074C10.4951 18.7461 10.9639 18.5654 11.5205 18.5654H14.8896C16.2178 18.5654 17.375 18.79 18.3613 19.2393C19.3477 19.6787 20.2607 20.4062 21.1006 21.4219L30.2998 32.6719C30.9248 33.4336 31.5742 33.9854 32.248 34.3271C32.9219 34.6592 33.7373 34.8252 34.6943 34.8252H37.3311V31.7344C37.3311 31.2852 37.458 30.9287 37.7119 30.665C37.9658 30.4014 38.3174 30.2695 38.7666 30.2695C38.9814 30.2695 39.1719 30.3086 39.3379 30.3867C39.5137 30.4551 39.6748 30.5479 39.8213 30.665L45.9297 35.8066C46.2715 36.0996 46.4424 36.4463 46.4424 36.8467C46.4424 37.2471 46.2715 37.5938 45.9297 37.8867L39.8213 43.0283C39.6748 43.1455 39.5137 43.2383 39.3379 43.3066C39.1719 43.375 38.9814 43.4092 38.7666 43.4092C38.3174 43.4092 37.9658 43.2822 37.7119 43.0283C37.458 42.7744 37.3311 42.418 37.3311 41.959V38.5898H34.6211C33.2832 38.5898 32.0723 38.3652 30.9883 37.916C29.9141 37.457 28.9229 36.6758 28.0146 35.5723L18.9619 24.498C18.3369 23.7266 17.6631 23.1748 16.9404 22.8428C16.2275 22.5107 15.4023 22.3447 14.4648 22.3447H11.5205C10.9639 22.3447 10.4951 22.1641 10.1143 21.8027C9.74316 21.4316 9.55762 20.9824 9.55762 20.4551Z",fill:"currentColor"})}),CI=q.forwardRef(TI),SI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M1.65755 6.64792C0 9.6077 0 13.5551 0 21.45V34.55C0 42.4449 0 46.3923 1.65755 49.3521C2.82902 51.4439 4.55612 53.171 6.64792 54.3424C9.6077 56 13.5551 56 21.45 56H34.55C42.4449 56 46.3923 56 49.3521 54.3424C51.4439 53.171 53.171 51.4439 54.3424 49.3521C56 46.3923 56 42.4449 56 34.55V21.45C56 13.5551 56 9.6077 54.3424 6.64792C53.171 4.55612 51.4439 2.82902 49.3521 1.65755C46.3923 0 42.4449 0 34.55 0H21.45C13.5551 0 9.6077 0 6.64792 1.65755C4.55612 2.82902 2.82902 4.55612 1.65755 6.64792ZM10.1143 35.3672C9.74316 35.7285 9.55762 36.1777 9.55762 36.7148C9.55762 37.2422 9.74316 37.6865 10.1143 38.0479C10.4951 38.4092 10.9639 38.5898 11.5205 38.5898H15.1387C16.4766 38.5898 17.5947 38.3701 18.4932 37.9307C19.3916 37.4814 20.2607 36.7539 21.1006 35.748L24.631 31.4331L28.0146 35.5723C28.9229 36.6758 29.9141 37.457 30.9883 37.916C32.0723 38.3652 33.2832 38.5898 34.6211 38.5898H37.3311V41.959C37.3311 42.418 37.458 42.7744 37.7119 43.0283C37.9658 43.2822 38.3174 43.4092 38.7666 43.4092C38.9814 43.4092 39.1719 43.375 39.3379 43.3066C39.5137 43.2383 39.6748 43.1455 39.8213 43.0283L45.9297 37.8867C46.2715 37.5938 46.4424 37.2471 46.4424 36.8467C46.4424 36.4463 46.2715 36.0996 45.9297 35.8066L39.8213 30.665C39.6748 30.5479 39.5137 30.4551 39.3379 30.3867C39.1719 30.3086 38.9814 30.2695 38.7666 30.2695C38.3174 30.2695 37.9658 30.4014 37.7119 30.665C37.458 30.9287 37.3311 31.2852 37.3311 31.7344V34.8252H34.6943C33.7373 34.8252 32.9219 34.6592 32.248 34.3271C31.5742 33.9854 30.9248 33.4336 30.2998 32.6719L26.9596 28.587L30.3291 24.4688C30.7588 23.9414 31.1689 23.5215 31.5596 23.209C31.96 22.8965 32.3896 22.6768 32.8486 22.5498C33.3076 22.4131 33.8496 22.3447 34.4746 22.3447H37.3311V25.4355C37.3311 25.8848 37.458 26.2412 37.7119 26.5049C37.9658 26.7588 38.3174 26.8857 38.7666 26.8857C38.9814 26.8857 39.1719 26.8516 39.3379 26.7832C39.5137 26.7148 39.6748 26.6221 39.8213 26.5049L45.9297 21.3486C46.2715 21.0654 46.4424 20.7236 46.4424 20.3232C46.4424 19.9131 46.2715 19.5664 45.9297 19.2832L39.8213 14.127C39.6748 14.0098 39.5137 13.917 39.3379 13.8486C39.1719 13.7803 38.9814 13.7461 38.7666 13.7461C38.3174 13.7461 37.9658 13.8779 37.7119 14.1416C37.458 14.3955 37.3311 14.7471 37.3311 15.1963V18.5654H34.6504C33.2051 18.5654 31.9551 18.7998 30.9004 19.2686C29.8457 19.7275 28.8008 20.5918 27.7656 21.8613L24.6192 25.7249L21.1006 21.4219C20.2607 20.4062 19.3477 19.6787 18.3613 19.2393C17.375 18.79 16.2178 18.5654 14.8896 18.5654H11.5205C10.9639 18.5654 10.4951 18.7461 10.1143 19.1074C9.74316 19.4688 9.55762 19.918 9.55762 20.4551C9.55762 20.9824 9.74316 21.4316 10.1143 21.8027C10.4951 22.1641 10.9639 22.3447 11.5205 22.3447H14.4648C15.4023 22.3447 16.2275 22.5107 16.9404 22.8428C17.6631 23.1748 18.3369 23.7266 18.9619 24.498L22.2965 28.5772L18.9619 32.6719C18.3369 33.4434 17.707 33.9951 17.0723 34.3271C16.4375 34.6592 15.6562 34.8252 14.7285 34.8252H11.5205C10.9639 34.8252 10.4951 35.0059 10.1143 35.3672Z",fill:"currentColor"})}),_I=q.forwardRef(SI),AI=(n,t)=>Y.jsx("svg",{width:36,height:36,viewBox:"0 0 36 36",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M7.83396 32.7793C7.49216 32.5254 7.2822 32.1836 7.20407 31.7539C7.13571 31.334 7.19919 30.8359 7.3945 30.2598L10.4121 21.3096L2.72165 15.7871C2.2236 15.4355 1.87692 15.0693 1.68161 14.6885C1.4863 14.2979 1.457 13.8975 1.59372 13.4873C1.73044 13.0869 1.98923 12.7891 2.37009 12.5938C2.75095 12.3887 3.24899 12.291 3.86423 12.3008L13.2832 12.3594L16.1543 3.36523C16.3398 2.7793 16.584 2.33496 16.8867 2.03223C17.1992 1.71973 17.5654 1.56348 17.9853 1.56348C18.415 1.56348 18.7812 1.71973 19.084 2.03223C19.3965 2.33496 19.6455 2.7793 19.831 3.36523L22.7021 12.3594L32.1211 12.3008C32.7363 12.291 33.2343 12.3887 33.6152 12.5938C33.9961 12.7891 34.2549 13.0869 34.3916 13.4873C34.5283 13.8975 34.499 14.2979 34.3037 14.6885C34.1084 15.0693 33.7617 15.4355 33.2636 15.7871L25.5732 21.3096L28.5908 30.2598C28.7861 30.8359 28.8447 31.334 28.7666 31.7539C28.6982 32.1836 28.4931 32.5254 28.1513 32.7793C27.8095 33.0527 27.4238 33.1504 26.9941 33.0723C26.5644 32.9941 26.1054 32.7793 25.6172 32.4277L17.9853 26.8174L10.3681 32.4277C9.87985 32.7793 9.42087 32.9941 8.99118 33.0723C8.56149 33.1504 8.17575 33.0527 7.83396 32.7793ZM10.4121 29.2344C10.4316 29.2637 10.4707 29.2588 10.5293 29.2197L17.1357 24.1221C17.4287 23.8877 17.7119 23.7705 17.9853 23.7705C18.2685 23.7705 18.5566 23.8877 18.8496 24.1221L25.456 29.2197C25.5146 29.2588 25.5537 29.2637 25.5732 29.2344C25.583 29.2148 25.583 29.1758 25.5732 29.1172L22.79 21.2656C22.7119 21.0312 22.6679 20.8213 22.6582 20.6357C22.6582 20.4404 22.707 20.2646 22.8047 20.1084C22.9121 19.9521 23.0732 19.8008 23.2881 19.6543L30.1728 14.9521C30.2314 14.9131 30.2558 14.874 30.2461 14.835C30.2363 14.8057 30.1924 14.791 30.1142 14.791L21.7939 14.9961C21.4131 15.0059 21.1152 14.9424 20.9004 14.8057C20.6855 14.6592 20.5244 14.3955 20.417 14.0146L18.0732 6.03125C18.0537 5.96289 18.0244 5.92871 17.9853 5.92871C17.956 5.92871 17.9316 5.96289 17.9121 6.03125L15.5683 14.0146C15.4609 14.3955 15.2998 14.6592 15.0849 14.8057C14.8701 14.9424 14.5722 15.0059 14.1914 14.9961L5.87107 14.791C5.79294 14.791 5.74899 14.8057 5.73923 14.835C5.72946 14.874 5.75388 14.9131 5.81247 14.9521L12.6972 19.6543C12.9121 19.8008 13.0683 19.9521 13.166 20.1084C13.2734 20.2646 13.3222 20.4404 13.3125 20.6357C13.3125 20.8213 13.2734 21.0312 13.1953 21.2656L10.4121 29.1172C10.3925 29.1758 10.3925 29.2148 10.4121 29.2344Z",fill:"currentColor"})}),EI=q.forwardRef(AI),wI=(n,t)=>Y.jsx("svg",{width:36,height:36,viewBox:"0 0 36 36",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M7.83396 32.7793C7.49216 32.5254 7.2822 32.1836 7.20407 31.7539C7.13571 31.334 7.19919 30.8359 7.3945 30.2598L10.4121 21.3096L2.72165 15.7871C2.2236 15.4355 1.87692 15.0693 1.68161 14.6885C1.4863 14.2979 1.457 13.8975 1.59372 13.4873C1.73044 13.0869 1.98923 12.7891 2.37009 12.5938C2.75095 12.3984 3.24899 12.3008 3.86423 12.3008H13.2832L16.1543 3.36523C16.3398 2.7793 16.584 2.33496 16.8867 2.03223C17.1992 1.71973 17.5654 1.56348 17.9853 1.56348C18.415 1.56348 18.7812 1.71973 19.084 2.03223C19.3965 2.33496 19.6455 2.7793 19.831 3.36523L22.7021 12.3008H32.1211C32.7363 12.3008 33.2343 12.3984 33.6152 12.5938C33.9961 12.7891 34.2549 13.0869 34.3916 13.4873C34.5283 13.8975 34.499 14.2979 34.3037 14.6885C34.1084 15.0693 33.7617 15.4355 33.2636 15.7871L25.5732 21.3096L28.5908 30.2598C28.7861 30.8359 28.8447 31.334 28.7666 31.7539C28.6982 32.1836 28.4931 32.5254 28.1513 32.7793C27.8095 33.0527 27.4238 33.1504 26.9941 33.0723C26.5644 32.9941 26.1054 32.7793 25.6172 32.4277L17.9853 26.8174L10.3681 32.4277C9.87985 32.7793 9.42087 32.9941 8.99118 33.0723C8.56149 33.1504 8.17575 33.0527 7.83396 32.7793Z",fill:"currentColor"})}),MI=q.forwardRef(wI);globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const RI=q.memo(n=>{const t=Re.c(12);let e,i,a,o,c;t[0]!==n?({uncheckedIcon:c,checkedIcon:i,checked:e,className:a,...o}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o,t[5]=c):(e=t[1],i=t[2],a=t[3],o=t[4],c=t[5]);let u;t[6]!==a?(u=on(a,lI.toggleIconButton),t[6]=a,t[7]=u):u=t[7];const f=e?i:c;let d;return t[8]!==o||t[9]!==u||t[10]!==f?(d=Y.jsx("button",{className:u,type:"button",...o,children:f}),t[8]=o,t[9]=u,t[10]=f,t[11]=d):d=t[11],d}),LI={lyrics:[uI,fI],playlist:[mI,gI],repeat:[vI,xI],shuffle:[CI,_I],star:[EI,MI],airplay:[L8,L8]},Ma=q.memo(n=>{const t=Re.c(15);let e,i,a,o;t[0]!==n?({type:o,checked:e,onClick:i,...a}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o):(e=t[1],i=t[2],a=t[3],o=t[4]);const[c,u]=LI[o];let f;t[5]!==c?(f=Y.jsx(c,{}),t[5]=c,t[6]=f):f=t[6];let d;t[7]!==u?(d=Y.jsx(u,{}),t[7]=u,t[8]=d):d=t[8];const m=e??!1;let g;return t[9]!==i||t[10]!==a||t[11]!==f||t[12]!==d||t[13]!==m?(g=Y.jsx(RI,{uncheckedIcon:f,checkedIcon:d,checked:m,onClick:i,...a}),t[9]=i,t[10]=a,t[11]=f,t[12]=d,t[13]=m,t[14]=g):g=t[14],g}),NI=(n,t)=>Y.jsx("svg",{width:32,height:40,viewBox:"0 0 32 40",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M14.9042 27.1802C14.4202 27.1802 14.0473 26.9897 13.595 26.5612L10.3815 23.5461C10.3339 23.5065 10.2863 23.4906 10.2228 23.4906H8.01703C6.70778 23.4906 5.99365 22.7527 5.99365 21.38V18.4442C5.99365 17.0715 6.70778 16.3257 8.01703 16.3257H10.2307C10.2863 16.3257 10.3418 16.3019 10.3815 16.2622L13.595 13.2709C14.079 12.8107 14.4361 12.6282 14.8883 12.6282C15.6104 12.6282 16.142 13.1915 16.142 13.8977V25.9344C16.142 26.6406 15.6104 27.1802 14.9042 27.1802Z",className:"speaker-bounce-1"})}),OI=q.forwardRef(NI),II=(n,t)=>Y.jsxs("svg",{width:43,height:40,viewBox:"0 0 43 40",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:[Y.jsx("path",{d:"M24.0403 27.1802C23.5642 27.1802 23.1913 26.9897 22.739 26.5612L19.5176 23.5461C19.4779 23.5065 19.4224 23.4906 19.3668 23.4906H17.161C15.8518 23.4906 15.1377 22.7527 15.1377 21.38V18.4442C15.1377 17.0715 15.8518 16.3257 17.161 16.3257H19.3668C19.4303 16.3257 19.4779 16.3019 19.5255 16.2622L22.739 13.2709C23.223 12.8107 23.5721 12.6282 24.0324 12.6282C24.7544 12.6282 25.286 13.1915 25.286 13.8977V25.9344C25.286 26.6406 24.7544 27.1802 24.0403 27.1802Z",className:"speaker-bounce-1"}),Y.jsx("path",{d:"M28.0948 23.6653C27.6028 23.3559 27.4996 22.7687 27.8964 22.1101C28.2931 21.4991 28.5232 20.7136 28.5232 19.8964C28.5232 19.0712 28.301 18.2856 27.8964 17.6826C27.4917 17.032 27.6028 16.4369 28.0948 16.1274C28.547 15.8418 29.1104 15.9529 29.404 16.3576C30.0863 17.3097 30.491 18.5713 30.491 19.8964C30.491 21.2214 30.0863 22.4831 29.404 23.4273C29.1104 23.8399 28.547 23.943 28.0948 23.6653Z",className:"speaker-bounce-2"}),Y.jsx("path",{d:"M31.6733 25.8711C31.1576 25.5696 31.0942 24.9428 31.4432 24.3794C32.2526 23.1257 32.7207 21.5468 32.7207 19.8964C32.7207 18.2459 32.2605 16.6591 31.4432 15.4133C31.0942 14.8499 31.1576 14.2231 31.6733 13.9137C32.1415 13.6439 32.7128 13.755 33.0143 14.2152C34.0855 15.7783 34.6885 17.8016 34.6885 19.8964C34.6885 21.9911 34.0775 23.9985 33.0143 25.5775C32.7128 26.0377 32.1415 26.1488 31.6733 25.8711Z",className:"speaker-bounce-3"}),Y.jsx("path",{d:"M35.2362 28.1007C34.7363 27.7992 34.6569 27.1803 34.9981 26.6249C36.1883 24.7286 36.9104 22.4196 36.9104 19.9122C36.9104 17.397 36.1883 15.0881 34.9981 13.1917C34.6569 12.6362 34.7363 12.0174 35.2362 11.7159C35.7123 11.4302 36.3073 11.5651 36.6088 12.0571C38.0133 14.2866 38.8702 16.9765 38.8702 19.9122C38.8702 22.8401 38.0291 25.5379 36.6088 27.7675C36.3073 28.2515 35.7123 28.3864 35.2362 28.1007Z",className:"speaker-bounce-4"})]}),DI=q.forwardRef(II);var ul={speakerAnimate:"LJEMWq_speakerAnimate",volumeControl:"LJEMWq_volumeControl"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const BI=n=>{const t=Re.c(9),e=q.useRef(n.value),i=q.useRef(null),a=q.useRef(null);let o,c;t[0]!==n.max||t[1]!==n.min||t[2]!==n.value?(o=()=>{e.current!==n.value&&(e.current=n.value,n.value<=n.min&&i.current?(i.current.classList.remove(ul.speakerAnimate),requestAnimationFrame(()=>{i.current?.classList?.add(ul.speakerAnimate)})):n.value>=n.max&&a.current&&(a.current.classList.remove(ul.speakerAnimate),requestAnimationFrame(()=>{a.current?.classList?.add(ul.speakerAnimate)})))},c=[n.value,n.min,n.max],t[0]=n.max,t[1]=n.min,t[2]=n.value,t[3]=o,t[4]=c):(o=t[3],c=t[4]),q.useEffect(o,c);let u,f;t[5]===Symbol.for("react.memo_cache_sentinel")?(u=Y.jsx(OI,{ref:i,color:"#FFFFFF"}),f=Y.jsx(DI,{ref:a,color:"#FFFFFF"}),t[5]=u,t[6]=f):(u=t[5],f=t[6]);let d;return t[7]!==n?(d=Y.jsx(cb,{className:ul.volumeControl,beforeIcon:u,afterIcon:f,changeOnDrag:!0,...n}),t[7]=n,t[8]=d):d=t[8],d},PI=(n,t)=>Y.jsxs("svg",{id:"vector",xmlns:"http://www.w3.org/2000/svg",width:32,height:32,viewBox:"0 0 134 134",fill:"currentColor",ref:t,...n,children:[Y.jsx("path",{d:"M62 60.0717C65.938 62.3453 67.9069 63.4821 68.5677 64.9662C69.1441 66.2608 69.1441 67.7391 68.5677 69.0336C67.9069 70.5177 65.938 71.6545 62 73.9281L41 86.0525C37.062 88.326 35.0931 89.4628 33.4774 89.293C32.0681 89.1449 30.7878 88.4057 29.9549 87.2593C29 85.945 29 83.6714 29 79.1243V54.8755C29 50.3284 29 48.0548 29.9549 46.7405C30.7878 45.5941 32.0681 44.8549 33.4774 44.7068C35.0931 44.537 37.062 45.6738 41 47.9473L62 60.0717Z",className:"amll-forward-left-arrow"}),Y.jsx("path",{d:"M62 60.0717C65.938 62.3453 67.9069 63.4821 68.5677 64.9662C69.1441 66.2608 69.1441 67.7391 68.5677 69.0336C67.9069 70.5177 65.938 71.6545 62 73.9281L41 86.0525C37.062 88.326 35.0931 89.4628 33.4774 89.293C32.0681 89.1449 30.7878 88.4057 29.9549 87.2593C29 85.945 29 83.6714 29 79.1243V54.8755C29 50.3284 29 48.0548 29.9549 46.7405C30.7878 45.5941 32.0681 44.8549 33.4774 44.7068C35.0931 44.537 37.062 45.6738 41 47.9473L62 60.0717Z",className:"amll-forward-left-standby"}),Y.jsx("path",{d:"M102 60.0717C105.938 62.3453 107.907 63.4821 108.568 64.9662C109.144 66.2608 109.144 67.7391 108.568 69.0336C107.907 70.5177 105.938 71.6545 102 73.9281L81 86.0525C77.062 88.326 75.0931 89.4628 73.4774 89.293C72.0681 89.1449 70.7878 88.4057 69.9549 87.2593C69 85.945 69 83.6714 69 79.1243V54.8755C69 50.3284 69 48.0548 69.9549 46.7405C70.7878 45.5941 72.0681 44.8549 73.4774 44.7068C75.0931 44.537 77.062 45.6738 81 47.9473L102 60.0717Z",className:"amll-forward-right-arrow"})]}),FI=q.forwardRef(PI),UI=(n,t)=>Y.jsx("svg",{id:"vector",width:38,height:38,viewBox:"0 0 38 38",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M8.46953 37C7.37801 37 6.56603 36.7271 6.03359 36.1814C5.51445 35.6489 5.25488 34.8502 5.25488 33.7854V4.21464C5.25488 3.14975 5.52111 2.35108 6.05355 1.81864C6.59931 1.27288 7.40463 1 8.46953 1H13.3813C14.4329 1 15.2249 1.27288 15.7574 1.81864C16.3031 2.35108 16.576 3.14975 16.576 4.21464V33.7854C16.576 34.8502 16.3031 35.6489 15.7574 36.1814C15.2249 36.7271 14.4329 37 13.3813 37H8.46953ZM24.6426 37C23.5644 37 22.759 36.7271 22.2266 36.1814C21.6942 35.6489 21.4279 34.8502 21.4279 33.7854V4.21464C21.4279 3.14975 21.6942 2.35108 22.2266 1.81864C22.7724 1.27288 23.5777 1 24.6426 1H29.5544C30.6193 1 31.4179 1.27288 31.9504 1.81864C32.4828 2.35108 32.7491 3.14975 32.7491 4.21464V33.7854C32.7491 34.8502 32.4828 35.6489 31.9504 36.1814C31.4179 36.7271 30.6193 37 29.5544 37H24.6426Z",fill:"currentColor",fillRule:"nonzero",id:"path_0"})}),kI=q.forwardRef(UI),zI=(n,t)=>Y.jsx("svg",{id:"vector",width:38,height:38,viewBox:"0 0 38 38",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M5.80762 32.4896V5.4925C5.80762 4.305 6.12305 3.41438 6.75391 2.82063C7.38477 2.22688 8.13932 1.93 9.01758 1.93C9.78451 1.93 10.5391 2.14029 11.2812 2.56086L33.7324 15.6605C34.5859 16.1553 35.223 16.6562 35.6436 17.1634C36.0641 17.6582 36.2744 18.2705 36.2744 19.0003C36.2744 19.7054 36.0641 20.3177 35.6436 20.8372C35.223 21.3444 34.5859 21.8392 33.7324 22.3216L11.2812 35.4212C10.5391 35.8542 9.78451 36.0706 9.01758 36.0706C8.13932 36.0706 7.38477 35.7676 6.75391 35.1614C6.12305 34.5677 5.80762 33.6771 5.80762 32.4896Z",fill:"currentColor",fillRule:"nonzero"})}),VI=q.forwardRef(zI),GI=(n,t)=>Y.jsxs("svg",{id:"vector",xmlns:"http://www.w3.org/2000/svg",width:32,height:32,viewBox:"0 0 134 134",fill:"currentColor",ref:t,...n,children:[Y.jsx("path",{d:"M72 60.0717C68.062 62.3453 66.0931 63.4821 65.4323 64.9662C64.8559 66.2608 64.8559 67.7391 65.4323 69.0336C66.0931 70.5177 68.062 71.6545 72 73.9281L93 86.0525C96.938 88.326 98.9069 89.4628 100.523 89.293C101.932 89.1449 103.212 88.4057 104.045 87.2593C105 85.945 105 83.6714 105 79.1243V54.8755C105 50.3284 105 48.0548 104.045 46.7405C103.212 45.5941 101.932 44.8549 100.523 44.7068C98.9069 44.537 96.938 45.6738 93 47.9473L72 60.0717Z",className:"amll-rewind-right-arrow"}),Y.jsx("path",{d:"M72 60.0717C68.062 62.3453 66.0931 63.4821 65.4323 64.9662C64.8559 66.2608 64.8559 67.7391 65.4323 69.0336C66.0931 70.5177 68.062 71.6545 72 73.9281L93 86.0525C96.938 88.326 98.9069 89.4628 100.523 89.293C101.932 89.1449 103.212 88.4057 104.045 87.2593C105 85.945 105 83.6714 105 79.1243V54.8755C105 50.3284 105 48.0548 104.045 46.7405C103.212 45.5941 101.932 44.8549 100.523 44.7068C98.9069 44.537 96.938 45.6738 93 47.9473L72 60.0717Z",className:"amll-rewind-right-standby"}),Y.jsx("path",{d:"M32 60.0717C28.062 62.3453 26.0931 63.4821 25.4323 64.9662C24.8559 66.2608 24.8559 67.7391 25.4323 69.0336C26.0931 70.5177 28.062 71.6545 32 73.9281L53 86.0525C56.938 88.326 58.9069 89.4628 60.5226 89.293C61.9319 89.1449 63.2122 88.4057 64.0451 87.2593C65 85.945 65 83.6714 65 79.1243V54.8755C65 50.3284 65 48.0548 64.0451 46.7405C63.2122 45.5941 61.9319 44.8549 60.5226 44.7068C58.9069 44.537 56.938 45.6738 53 47.9473L32 60.0717Z",className:"amll-rewind-left-arrow"})]}),jI=q.forwardRef(GI),HI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M14.2495 28.9956C13.6519 28.9956 13.1465 28.7891 12.7334 28.376C12.3203 27.9541 12.1138 27.4531 12.1138 26.873V25.3438C12.1138 23.832 12.4565 22.5312 13.1421 21.4414C13.8276 20.3516 14.8076 19.5166 16.082 18.9365C17.3564 18.3477 18.877 18.0532 20.6436 18.0532H30.3599V15.4033C30.3599 14.9111 30.4961 14.5288 30.7686 14.2563C31.041 13.9751 31.4146 13.8345 31.8892 13.8345C32.1177 13.8345 32.3198 13.874 32.4956 13.9531C32.6714 14.0234 32.8296 14.1113 32.9702 14.2168L38.7578 19.1343C39.1182 19.4331 39.2939 19.7979 39.2852 20.2285C39.2852 20.6504 39.1094 21.0107 38.7578 21.3096L32.9702 26.2271C32.8296 26.3501 32.6714 26.4468 32.4956 26.5171C32.3198 26.5874 32.1177 26.6226 31.8892 26.6226C31.4146 26.6226 31.041 26.4819 30.7686 26.2007C30.4961 25.9194 30.3599 25.5415 30.3599 25.0669V22.1929H20.459C19.1846 22.1929 18.1826 22.5269 17.4531 23.1948C16.7236 23.8628 16.3589 24.7812 16.3589 25.9502V26.873C16.3589 27.4531 16.1523 27.9541 15.7393 28.376C15.3262 28.7891 14.8296 28.9956 14.2495 28.9956ZM41.7505 26.7017C42.3306 26.7017 42.8271 26.9082 43.2402 27.3213C43.6621 27.7344 43.873 28.2354 43.873 28.8242V30.3535C43.873 31.8652 43.5303 33.166 42.8447 34.2559C42.1592 35.3457 41.1792 36.1851 39.9048 36.7739C38.6304 37.354 37.1055 37.644 35.3301 37.644H25.627V40.2676C25.627 40.751 25.4907 41.1333 25.2183 41.4146C24.9458 41.6958 24.5723 41.8364 24.0977 41.8364C23.8691 41.8364 23.6626 41.7969 23.478 41.7178C23.3022 41.6475 23.1484 41.5552 23.0166 41.4409L17.2158 36.5366C16.873 36.2466 16.6973 35.8862 16.6885 35.4556C16.6885 35.0249 16.8643 34.6558 17.2158 34.3481L23.0166 29.4307C23.1484 29.3164 23.3022 29.2241 23.478 29.1538C23.6626 29.0835 23.8691 29.0483 24.0977 29.0483C24.5723 29.0483 24.9458 29.189 25.2183 29.4702C25.4907 29.7427 25.627 30.125 25.627 30.6172V33.4912H35.5278C36.8022 33.4912 37.8042 33.1616 38.5337 32.5024C39.2632 31.8345 39.6279 30.916 39.6279 29.7471V28.8242C39.6279 28.2354 39.8301 27.7344 40.2344 27.3213C40.6475 26.9082 41.1528 26.7017 41.7505 26.7017Z",fill:"white"})}),qI=q.forwardRef(HI),XI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.5805 0H18.4195C12.0146 0 9.69208 0.666878 7.35056 1.91914C5.00904 3.1714 3.1714 5.00904 1.91914 7.35056C0.666879 9.69208 0 12.0146 0 18.4195V37.5805C0 43.9854 0.666879 46.3079 1.91914 48.6494C3.1714 50.991 5.00904 52.8286 7.35056 54.0809C9.69208 55.3331 12.0146 56 18.4195 56H37.5805C43.9854 56 46.3079 55.3331 48.6495 54.0809C50.991 52.8286 52.8286 50.991 54.0809 48.6494C55.3331 46.3079 56 43.9854 56 37.5805V18.4195C56 12.0146 55.3331 9.69208 54.0809 7.35056C52.8286 5.00904 50.991 3.1714 48.6495 1.91914C46.3079 0.666878 43.9854 0 37.5805 0ZM12.7334 28.376C13.1465 28.7891 13.6519 28.9956 14.2495 28.9956C14.8296 28.9956 15.3262 28.7891 15.7393 28.376C16.1523 27.9541 16.3589 27.4531 16.3589 26.873V25.9502C16.3589 24.7812 16.7236 23.8628 17.4531 23.1948C18.1826 22.5269 19.1846 22.1929 20.459 22.1929H30.3599V25.0669C30.3599 25.5415 30.4961 25.9194 30.7686 26.2007C31.041 26.4819 31.4146 26.6226 31.8892 26.6226C32.1177 26.6226 32.3198 26.5874 32.4956 26.5171C32.6714 26.4468 32.8296 26.3501 32.9702 26.2271L38.7578 21.3096C39.1094 21.0107 39.2852 20.6504 39.2852 20.2285C39.2939 19.7979 39.1182 19.4331 38.7578 19.1343L32.9702 14.2168C32.8296 14.1113 32.6714 14.0234 32.4956 13.9531C32.3198 13.874 32.1177 13.8345 31.8892 13.8345C31.4146 13.8345 31.041 13.9751 30.7686 14.2563C30.4961 14.5288 30.3599 14.9111 30.3599 15.4033V18.0532H20.6436C18.877 18.0532 17.3564 18.3477 16.082 18.9365C14.8076 19.5166 13.8276 20.3516 13.1421 21.4414C12.4565 22.5312 12.1138 23.832 12.1138 25.3438V26.873C12.1138 27.4531 12.3203 27.9541 12.7334 28.376ZM43.2402 27.3213C42.8271 26.9082 42.3306 26.7017 41.7505 26.7017C41.1528 26.7017 40.6475 26.9082 40.2344 27.3213C39.8301 27.7344 39.6279 28.2354 39.6279 28.8242V29.7471C39.6279 30.916 39.2632 31.8345 38.5337 32.5024C37.8042 33.1616 36.8022 33.4912 35.5278 33.4912H25.627V30.6172C25.627 30.125 25.4907 29.7427 25.2183 29.4702C24.9458 29.189 24.5723 29.0483 24.0977 29.0483C23.8691 29.0483 23.6626 29.0835 23.478 29.1538C23.3022 29.2241 23.1484 29.3164 23.0166 29.4307L17.2158 34.3481C16.8643 34.6558 16.6885 35.0249 16.6885 35.4556C16.6973 35.8862 16.873 36.2466 17.2158 36.5366L23.0166 41.4409C23.1484 41.5552 23.3022 41.6475 23.478 41.7178C23.6626 41.7969 23.8691 41.8364 24.0977 41.8364C24.5723 41.8364 24.9458 41.6958 25.2183 41.4146C25.4907 41.1333 25.627 40.751 25.627 40.2676V37.644H35.3301C37.1055 37.644 38.6304 37.354 39.9048 36.7739C41.1792 36.1851 42.1592 35.3457 42.8447 34.2559C43.5303 33.166 43.873 31.8652 43.873 30.3535V28.8242C43.873 28.2354 43.6621 27.7344 43.2402 27.3213Z",fill:"white"})}),YI=q.forwardRef(XI),$I=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.5805 0H18.4195C12.0146 0 9.69208 0.666878 7.35056 1.91914C5.00904 3.1714 3.1714 5.00904 1.91914 7.35056C0.666879 9.69208 0 12.0146 0 18.4195V37.5805C0 43.9854 0.666879 46.3079 1.91914 48.6494C3.1714 50.991 5.00904 52.8286 7.35056 54.0809C9.69208 55.3331 12.0146 56 18.4195 56H37.5805C43.9854 56 46.3079 55.3331 48.6495 54.0809C50.991 52.8286 52.8286 50.991 54.0809 48.6494C55.3331 46.3079 56 43.9854 56 37.5805V18.4195C56 12.0146 55.3331 9.69208 54.0809 7.35056C52.8286 5.00904 50.991 3.1714 48.6495 1.91914C46.3079 0.666878 43.9854 0 37.5805 0ZM13.2212 28.4946C13.5552 28.8198 13.9858 28.9824 14.5132 28.9824C15.0405 28.9824 15.4668 28.8198 15.792 28.4946C16.1172 28.1606 16.2798 27.73 16.2798 27.2026V26.1743C16.2798 25.0142 16.6401 24.1001 17.3608 23.4321C18.0903 22.7554 19.0791 22.417 20.3271 22.417H26.5103V25.3701C26.5103 25.7744 26.6245 26.0952 26.853 26.3325C27.0903 26.561 27.4111 26.6753 27.8154 26.6753C28 26.6753 28.1714 26.6445 28.3296 26.583C28.4878 26.5215 28.6284 26.438 28.7515 26.3325L34.2622 21.6919C34.5698 21.437 34.7236 21.1294 34.7236 20.769C34.7236 20.3999 34.5698 20.0879 34.2622 19.833L28.7515 15.1924C28.6284 15.0869 28.4878 15.0034 28.3296 14.9419C28.1714 14.8804 28 14.8496 27.8154 14.8496C27.4111 14.8496 27.0903 14.9683 26.853 15.2056C26.6245 15.4341 26.5103 15.7549 26.5103 16.168V18.9629H20.5249C18.9165 18.9629 17.5322 19.2354 16.3721 19.7803C15.2119 20.3164 14.3154 21.0898 13.6826 22.1006C13.0498 23.1113 12.7334 24.3198 12.7334 25.7261V27.2026C12.7334 27.73 12.896 28.1606 13.2212 28.4946ZM42.7393 27.6509C42.4141 27.3257 41.9878 27.1631 41.4604 27.1631C40.9331 27.1631 40.5068 27.3257 40.1816 27.6509C39.8564 27.9761 39.6938 28.4067 39.6938 28.9429V29.9712C39.6938 31.1313 39.3291 32.0454 38.5996 32.7134C37.8789 33.3813 36.8945 33.7153 35.6465 33.7153H25.6138V30.7754C25.6138 30.3623 25.4995 30.0415 25.271 29.813C25.0425 29.5757 24.7261 29.457 24.3218 29.457C24.1284 29.457 23.9526 29.4878 23.7944 29.5493C23.6362 29.6108 23.4956 29.6943 23.3726 29.7998L17.8618 34.4404C17.563 34.7041 17.4136 35.0161 17.4136 35.3765C17.4136 35.7368 17.563 36.0444 17.8618 36.2993L23.3726 40.9399C23.4956 41.0454 23.6362 41.1289 23.7944 41.1904C23.9526 41.252 24.1284 41.2827 24.3218 41.2827C24.7261 41.2827 25.0425 41.1641 25.271 40.9268C25.4995 40.6982 25.6138 40.3818 25.6138 39.9775V37.1826H35.4487C37.0483 37.1826 38.4282 36.9146 39.5884 36.3784C40.7573 35.8335 41.6582 35.0557 42.291 34.0449C42.9238 33.0254 43.2402 31.8169 43.2402 30.4194V28.9429C43.2402 28.4067 43.0732 27.9761 42.7393 27.6509ZM40.2871 24.1968C40.6035 24.4868 41.021 24.6318 41.5396 24.6318C42.0669 24.6318 42.4844 24.4868 42.792 24.1968C43.1084 23.9067 43.2666 23.4629 43.2666 22.8652V16.603C43.2666 16.0142 43.0776 15.5396 42.6997 15.1792C42.3306 14.8188 41.856 14.6387 41.2759 14.6387C40.8188 14.6387 40.4189 14.709 40.0762 14.8496C39.7422 14.9902 39.3994 15.188 39.0479 15.4429L37.2812 16.7612C37.0176 16.9458 36.8374 17.1216 36.7407 17.2886C36.644 17.4556 36.5957 17.6533 36.5957 17.8818C36.5957 18.207 36.7012 18.4707 36.9121 18.6729C37.123 18.875 37.3911 18.9761 37.7163 18.9761C38.0151 18.9761 38.2832 18.8838 38.5205 18.6992L39.707 17.7632H39.8125V22.8652C39.8125 23.4629 39.9707 23.9067 40.2871 24.1968Z",fill:"white"})}),WI=q.forwardRef($I),ZI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{d:"M10.624 36.3125C10.624 35.75 10.8218 35.2754 11.2173 34.8887C11.6216 34.4932 12.1094 34.2954 12.6807 34.2954H15.4756C16.3896 34.2954 17.1455 34.1372 17.7432 33.8208C18.3496 33.5044 18.9341 32.9946 19.4966 32.2915L27.3936 22.3379C28.3955 21.0811 29.4282 20.2285 30.4917 19.7803C31.5552 19.332 32.79 19.1079 34.1963 19.1079H36.4243V16.1548C36.4243 15.6714 36.5605 15.2935 36.833 15.021C37.1055 14.7397 37.479 14.5991 37.9536 14.5991C38.1821 14.5991 38.3843 14.6343 38.5601 14.7046C38.7446 14.7749 38.9072 14.8672 39.0479 14.9814L44.8223 19.8857C45.1826 20.1846 45.3628 20.5493 45.3628 20.98C45.3628 21.4106 45.1826 21.7754 44.8223 22.0742L39.0479 26.9917C38.9072 27.106 38.7446 27.2026 38.5601 27.2817C38.3843 27.3521 38.1821 27.3872 37.9536 27.3872C37.479 27.3872 37.1055 27.2466 36.833 26.9653C36.5605 26.6841 36.4243 26.3018 36.4243 25.8184V23.1421H33.9194C33.3218 23.1421 32.8076 23.2036 32.377 23.3267C31.9551 23.4497 31.564 23.6562 31.2036 23.9463C30.8521 24.2275 30.4829 24.6143 30.0962 25.1064L21.606 35.7061C20.8853 36.6113 20.0986 37.2749 19.2461 37.6968C18.3936 38.1099 17.3389 38.3164 16.082 38.3164H12.6807C12.1094 38.3164 11.6216 38.123 11.2173 37.7363C10.8218 37.3496 10.624 36.875 10.624 36.3125ZM10.624 21.125C10.624 20.5625 10.8218 20.0879 11.2173 19.7012C11.6216 19.3057 12.1094 19.1079 12.6807 19.1079H15.7261C16.9829 19.1079 18.0947 19.3188 19.0615 19.7407C20.0371 20.1538 20.8853 20.8174 21.606 21.7314L30.0435 32.2783C30.5972 32.9727 31.1992 33.4824 31.8496 33.8076C32.5 34.1328 33.291 34.2954 34.2227 34.2954H36.4243V31.5664C36.4243 31.083 36.5605 30.7007 36.833 30.4194C37.1055 30.1382 37.479 29.9976 37.9536 29.9976C38.1821 29.9976 38.3843 30.0371 38.5601 30.1162C38.7446 30.1865 38.9072 30.2832 39.0479 30.4062L44.8223 35.2974C45.1826 35.5962 45.3628 35.9609 45.3628 36.3916C45.3628 36.8223 45.1826 37.187 44.8223 37.4858L39.0479 42.3901C38.9072 42.5132 38.7446 42.6099 38.5601 42.6802C38.3843 42.7593 38.1821 42.7988 37.9536 42.7988C37.479 42.7988 37.1055 42.6582 36.833 42.377C36.5605 42.0957 36.4243 41.7134 36.4243 41.23V38.3164H34.1699C32.9043 38.3164 31.7222 38.1011 30.6235 37.6704C29.5249 37.231 28.5625 36.4927 27.7363 35.4556L19.4966 25.146C18.9341 24.4429 18.2925 23.9331 17.5718 23.6167C16.8599 23.3003 16.0381 23.1421 15.1064 23.1421H12.6807C12.1094 23.1421 11.6216 22.9443 11.2173 22.5488C10.8218 22.1533 10.624 21.6787 10.624 21.125Z",fill:"white"})}),KI=q.forwardRef(ZI),QI=(n,t)=>Y.jsx("svg",{width:56,height:56,viewBox:"0 0 56 56",fill:"none",xmlns:"http://www.w3.org/2000/svg",ref:t,...n,children:Y.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.5805 0H18.4195C12.0146 0 9.69208 0.666878 7.35056 1.91914C5.00904 3.1714 3.1714 5.00904 1.91914 7.35056C0.666879 9.69208 0 12.0146 0 18.4195V37.5805C0 43.9854 0.666879 46.3079 1.91914 48.6494C3.1714 50.991 5.00904 52.8286 7.35056 54.0809C9.69208 55.3331 12.0146 56 18.4195 56H37.5805C43.9854 56 46.3079 55.3331 48.6495 54.0809C50.991 52.8286 52.8286 50.991 54.0809 48.6494C55.3331 46.3079 56 43.9854 56 37.5805V18.4195C56 12.0146 55.3331 9.69208 54.0809 7.35056C52.8286 5.00904 50.991 3.1714 48.6495 1.91914C46.3079 0.666878 43.9854 0 37.5805 0ZM11.2173 34.8887C10.8218 35.2754 10.624 35.75 10.624 36.3125C10.624 36.875 10.8218 37.3496 11.2173 37.7363C11.6216 38.123 12.1094 38.3164 12.6807 38.3164H16.082C17.3389 38.3164 18.3936 38.1099 19.2461 37.6968C20.0986 37.2749 20.8853 36.6113 21.606 35.7061L24.7747 31.75L27.7363 35.4556C28.5625 36.4927 29.5249 37.231 30.6235 37.6704C31.7222 38.1011 32.9043 38.3164 34.1699 38.3164H36.4243V41.23C36.4243 41.7134 36.5605 42.0957 36.833 42.377C37.1055 42.6582 37.479 42.7988 37.9536 42.7988C38.1821 42.7988 38.3843 42.7593 38.5601 42.6802C38.7446 42.6099 38.9072 42.5132 39.0479 42.3901L44.8223 37.4858C45.1826 37.187 45.3628 36.8223 45.3628 36.3916C45.3628 35.9609 45.1826 35.5962 44.8223 35.2974L39.0479 30.4062C38.9072 30.2832 38.7446 30.1865 38.5601 30.1162C38.3843 30.0371 38.1821 29.9976 37.9536 29.9976C37.479 29.9976 37.1055 30.1382 36.833 30.4194C36.5605 30.7007 36.4243 31.083 36.4243 31.5664V34.2954H34.2227C33.291 34.2954 32.5 34.1328 31.8496 33.8076C31.1992 33.4824 30.5972 32.9727 30.0435 32.2783L27.1993 28.7231L30.0962 25.1064C30.4829 24.6143 30.8521 24.2275 31.2036 23.9463C31.564 23.6562 31.9551 23.4497 32.377 23.3267C32.8076 23.2036 33.3218 23.1421 33.9194 23.1421H36.4243V25.8184C36.4243 26.3018 36.5605 26.6841 36.833 26.9653C37.1055 27.2466 37.479 27.3872 37.9536 27.3872C38.1821 27.3872 38.3843 27.3521 38.5601 27.2817C38.7446 27.2026 38.9072 27.106 39.0479 26.9917L44.8223 22.0742C45.1826 21.7754 45.3628 21.4106 45.3628 20.98C45.3628 20.5493 45.1826 20.1846 44.8223 19.8857L39.0479 14.9814C38.9072 14.8672 38.7446 14.7749 38.5601 14.7046C38.3843 14.6343 38.1821 14.5991 37.9536 14.5991C37.479 14.5991 37.1055 14.7397 36.833 15.021C36.5605 15.2935 36.4243 15.6714 36.4243 16.1548V19.1079H34.1963C32.79 19.1079 31.5552 19.332 30.4917 19.7803C29.4282 20.2285 28.3955 21.0811 27.3936 22.3379L24.7534 25.6657L21.606 21.7314C20.8853 20.8174 20.0371 20.1538 19.0615 19.7407C18.0947 19.3188 16.9829 19.1079 15.7261 19.1079H12.6807C12.1094 19.1079 11.6216 19.3057 11.2173 19.7012C10.8218 20.0879 10.624 20.5625 10.624 21.125C10.624 21.6787 10.8218 22.1533 11.2173 22.5488C11.6216 22.9443 12.1094 23.1421 12.6807 23.1421H15.1064C16.0381 23.1421 16.8599 23.3003 17.5718 23.6167C18.2925 23.9331 18.9341 24.4429 19.4966 25.146L22.3415 28.7056L19.4966 32.2915C18.9341 32.9946 18.3496 33.5044 17.7432 33.8208C17.1455 34.1372 16.3896 34.2954 15.4756 34.2954H12.6807C12.1094 34.2954 11.6216 34.4932 11.2173 34.8887Z",fill:"white"})}),JI=q.forwardRef(QI);globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const Qn=n=>({}),F2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onClickControlThumbAtom",zt(Qn()));F2.debugLabel="onClickControlThumbAtom";const hb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onClickAudioQualityTagAtom",zt(Qn()));hb.debugLabel="onClickAudioQualityTagAtom";const U2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onRequestOpenMenuAtom",zt(Qn()));U2.debugLabel="onRequestOpenMenuAtom";const k2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onPlayOrResumeAtom",zt(Qn()));k2.debugLabel="onPlayOrResumeAtom";const z2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onRequestPrevSongAtom",zt(Qn()));z2.debugLabel="onRequestPrevSongAtom";const V2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onRequestNextSongAtom",zt(Qn()));V2.debugLabel="onRequestNextSongAtom";const G2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onSeekPositionAtom",zt(Qn()));G2.debugLabel="onSeekPositionAtom";const j2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onLyricLineClickAtom",zt(Qn()));j2.debugLabel="onLyricLineClickAtom";const fb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onLyricLineContextMenuAtom",zt(Qn()));fb.debugLabel="onLyricLineContextMenuAtom";const H2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onChangeVolumeAtom",zt(Qn()));H2.debugLabel="onChangeVolumeAtom";const q2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onToggleShuffleAtom",zt(Qn()));q2.debugLabel="onToggleShuffleAtom";const X2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/callbacks.ts/onCycleRepeatModeAtom",zt(Qn()));X2.debugLabel="onCycleRepeatModeAtom";globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};let tD=(function(n){return n.Controls="controls",n.FFT="fft",n.None="none",n})({}),eD=(function(n){return n.Tiny="tiny",n.ExtraSmall="extra-small",n.Small="small",n.Medium="medium",n.Large="large",n.ExtraLarge="extra-large",n.Huge="huge",n})({});const nD=()=>{switch(localStorage.getItem("amll-react-full.lyricPlayerImplementation")){default:return{lyricPlayer:ob}}},db=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricPlayerImplementationAtom",zt(nD()));db.debugLabel="lyricPlayerImplementationAtom";const mb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricLineBlurEffectAtom",xe("amll-react-full.enableLyricLineBlurEffect",!0));mb.debugLabel="enableLyricLineBlurEffectAtom";const pb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricLineScaleEffectAtom",xe("amll-react-full.enableLyricLineScaleEffect",!0));pb.debugLabel="enableLyricLineScaleEffectAtom";const gb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricLineSpringAnimationAtom",xe("amll-react-full.enableLyricLineSpringAnimation",!0));gb.debugLabel="enableLyricLineSpringAnimationAtom";const Y2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricTranslationLineAtom",xe("amll-react-full.enableLyricTranslationLine",!0));Y2.debugLabel="enableLyricTranslationLineAtom";const $2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricRomanLineAtom",xe("amll-react-full.enableLyricRomanLine",!0));$2.debugLabel="enableLyricRomanLineAtom";const yb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/enableLyricSwapTransRomanLineAtom",xe("amll-react-full.enableLyricSwapTransRomanLine",!1));yb.debugLabel="enableLyricSwapTransRomanLineAtom";const vb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricWordFadeWidthAtom",xe("amll-react-full.lyricWordFadeWidth",.5));vb.debugLabel="lyricWordFadeWidthAtom";const Ih=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricFontFamilyAtom",xe("amll-react-full.lyricFontFamily",""));Ih.debugLabel="lyricFontFamilyAtom";const Dh=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricFontWeightAtom",xe("amll-react-full.lyricFontWeight",600));Dh.debugLabel="lyricFontWeightAtom";const Bh=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricLetterSpacingAtom",xe("amll-react-full.lyricLetterSpacing","normal"));Bh.debugLabel="lyricLetterSpacingAtom";const W2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricSizePresetAtom",xe("amll-react-full.lyricSizePreset","medium"));W2.debugLabel="lyricSizePresetAtom";const Z2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/playerControlsTypeAtom",xe("amll-react-full.playerControlsType","controls"));Z2.debugLabel="playerControlsTypeAtom";const K2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showMusicNameAtom",xe("amll-react-full.showMusicName",!0));K2.debugLabel="showMusicNameAtom";const bb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/verticalCoverLayoutAtom",xe("amll-react-full.verticalCoverLayout","auto"));bb.debugLabel="verticalCoverLayoutAtom";const xb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showMusicArtistsAtom",xe("amll-react-full.showMusicArtists",!0));xb.debugLabel="showMusicArtistsAtom";const Tb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showMusicAlbumAtom",xe("amll-react-full.showMusicAlbum",!1));Tb.debugLabel="showMusicAlbumAtom";const Cb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showVolumeControlAtom",xe("amll-react-full.showVolumeControl",!0));Cb.debugLabel="showVolumeControlAtom";const Sb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showBottomControlAtom",xe("amll-react-full.showBottomControl",!0));Sb.debugLabel="showBottomControlAtom";const iD=()=>{switch(localStorage.getItem("amll-react-full.lyricBackgroundRenderer")){case"pixi":return{renderer:ib};case"css-bg":return{renderer:"css-bg"};default:return{renderer:Oh}}},$l=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricBackgroundRendererAtom",zt(iD()));$l.debugLabel="lyricBackgroundRendererAtom";const Q2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/cssBackgroundPropertyAtom",xe("amll-player.cssBackgroundProperty","#111111"));Q2.debugLabel="cssBackgroundPropertyAtom";const _b=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricBackgroundFPSAtom",xe("amll-react-full.lyricBackgroundFPS",60));_b.debugLabel="lyricBackgroundFPSAtom";const Ab=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricBackgroundRenderScaleAtom",xe("amll-react-full.lyricBackgroundRenderScale",1));Ab.debugLabel="lyricBackgroundRenderScaleAtom";const Eb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/lyricBackgroundStaticModeAtom",xe("amll-react-full.lyricBackgroundStaticMode",!1));Eb.debugLabel="lyricBackgroundStaticModeAtom";const Ph=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/isLyricPageOpenedAtom",zt(!1));Ph.debugLabel="isLyricPageOpenedAtom";const J2=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/hideLyricViewAtom",xe("amll-react-full.hideLyricView",!1));J2.debugLabel="hideLyricViewAtom";const wb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/showRemainingTimeAtom",xe("amll-react-full.showRemainingTime",!0));wb.debugLabel="showRemainingTimeAtom";const sD=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/configAtoms.ts/fftDataRangeAtom",xe("amll-react-full.fftDataRange",[80,2e3]));sD.debugLabel="fftDataRangeAtom";globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const Mb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/isShuffleActiveAtom",zt(!1));Mb.debugLabel="isShuffleActiveAtom";const Rb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/repeatModeAtom",zt("off"));Rb.debugLabel="repeatModeAtom";const rD=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/isShuffleEnabledAtom",zt(!0));rD.debugLabel="isShuffleEnabledAtom";const aD=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/isRepeatEnabledAtom",zt(!0));aD.debugLabel="isRepeatEnabledAtom";const Lb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/toggleShuffleActionAtom",zt(null,n=>{n(q2).onEmit?.()}));Lb.debugLabel="toggleShuffleActionAtom";const Nb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/controlsAtoms.ts/cycleRepeatModeActionAtom",zt(null,n=>{n(X2).onEmit?.()}));Nb.debugLabel="cycleRepeatModeActionAtom";globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const oD=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicIdAtom",zt(""));oD.debugLabel="musicIdAtom";const t3=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicNameAtom",zt("未知歌曲"));t3.debugLabel="musicNameAtom";const e3=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicArtistsAtom",zt([{name:"未知创作者",id:"unknown"}]));e3.debugLabel="musicArtistsAtom";const Ob=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicAlbumNameAtom",zt("未知专辑"));Ob.debugLabel="musicAlbumNameAtom";const n3=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicCoverAtom",zt(""));n3.debugLabel="musicCoverAtom";const Ib=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicCoverIsVideoAtom",zt(!1));Ib.debugLabel="musicCoverIsVideoAtom";const Wl=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicDurationAtom",zt(0));Wl.debugLabel="musicDurationAtom";const Fr=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicPlayingAtom",zt(!1));Fr.debugLabel="musicPlayingAtom";const Dr=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicPlayingPositionAtom",zt(0));Dr.debugLabel="musicPlayingPositionAtom";const i3=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicVolumeAtom",xe("amll-react-full.musicVolumeAtom",.5,void 0,{getOnInit:!0}));i3.debugLabel="musicVolumeAtom";const Ga=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicLyricLinesAtom",zt([]));Ga.debugLabel="musicLyricLinesAtom";const lD=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicQualityAtom",zt({type:"none",codec:"unknown",channels:2,sampleRate:44100,sampleFormat:"s16"}));lD.debugLabel="musicQualityAtom";const Db=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/musicQualityTagAtom",zt(null));Db.debugLabel="musicQualityTagAtom";const Bb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/fftDataAtom",zt([]));Bb.debugLabel="fftDataAtom";const Pb=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/states/dataAtoms.ts/lowFreqVolumeAtom",zt(1));Pb.debugLabel="lowFreqVolumeAtom";var sn={autoLyricLayout:"UjzbJG_autoLyricLayout",bigControls:"UjzbJG_bigControls",bigMusicInfo:"UjzbJG_bigMusicInfo",bigVolumeControl:"UjzbJG_bigVolumeControl",controls:"UjzbJG_controls",hideLyric:"UjzbJG_hideLyric",horizontalControls:"UjzbJG_horizontalControls",progressBarLabels:"UjzbJG_progressBarLabels",qualityTag:"UjzbJG_qualityTag",smallMusicInfo:"UjzbJG_smallMusicInfo",songMediaButton:"UjzbJG_songMediaButton",songMediaPlayButton:"UjzbJG_songMediaPlayButton"};globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};const U0=n=>{const t=Re.c(15),{className:e,style:i}=n,a=Tt(t3),o=Tt(e3),c=Tt(Ob),u=Tt(U2).onEmit,f=Tt(K2),d=Tt(xb),m=Tt(Tb),g=Tt(Ih),v=Tt(Dh),x=Tt(Bh),b=g||void 0,C=v||void 0,T=x||void 0;let S;t[0]!==i||t[1]!==b||t[2]!==C||t[3]!==T?(S={...i,fontFamily:b,fontWeight:C,letterSpacing:T},t[0]=i,t[1]=b,t[2]=C,t[3]=T,t[4]=S):S=t[4];const L=S,R=f?a:void 0;let P;t[5]!==o||t[6]!==d?(P=d?o.map(mD):void 0,t[5]=o,t[6]=d,t[7]=P):P=t[7];const w=m?c:void 0;let F;return t[8]!==e||t[9]!==L||t[10]!==u||t[11]!==R||t[12]!==P||t[13]!==w?(F=Y.jsx(eI,{className:e,style:L,name:R,artists:P,album:w,onMenuButtonClicked:u}),t[8]=e,t[9]=L,t[10]=u,t[11]=R,t[12]=P,t[13]=w,t[14]=F):F=t[14],F},cD=n=>{const t=Re.c(28),{showOtherButtons:e}=n,i=Tt(Fr),a=Tt(z2).onEmit,o=Tt(V2).onEmit,c=Tt(k2).onEmit,u=Tt(Mb),f=Tt(Rb),d=Ju(Lb),m=Ju(Nb);let g;t[0]===Symbol.for("react.memo_cache_sentinel")?(g={width:"1.3em",height:"1.3em"},t[0]=g):g=t[0];const v=g;let x;t[1]!==f?(x=()=>{switch(f){case"one":return Y.jsx(WI,{color:"#ffffffff",style:v});case"all":return Y.jsx(YI,{color:"#ffffffff",style:v});default:return Y.jsx(qI,{color:"#ffffffff",style:v})}},t[1]=f,t[2]=x):x=t[2];const b=x;let C;t[3]!==u||t[4]!==e||t[5]!==d?(C=e&&Y.jsx(ll,{className:sn.songMediaButton,onClick:d,children:u?Y.jsx(JI,{color:"#ffffffff",style:v}):Y.jsx(KI,{color:"#ffffffff",style:v})}),t[3]=u,t[4]=e,t[5]=d,t[6]=C):C=t[6];let T;t[7]===Symbol.for("react.memo_cache_sentinel")?(T=Y.jsx(jI,{color:"#FFFFFF"}),t[7]=T):T=t[7];let S;t[8]!==a?(S=Y.jsx(ll,{className:sn.songMediaButton,onClick:a,children:T}),t[8]=a,t[9]=S):S=t[9];let L;t[10]!==i?(L=i?Y.jsx(kI,{color:"#FFFFFF"}):Y.jsx(VI,{color:"#FFFFFF"}),t[10]=i,t[11]=L):L=t[11];let R;t[12]!==c||t[13]!==L?(R=Y.jsx(ll,{className:sn.songMediaPlayButton,onClick:c,children:L}),t[12]=c,t[13]=L,t[14]=R):R=t[14];let P;t[15]===Symbol.for("react.memo_cache_sentinel")?(P=Y.jsx(FI,{color:"#FFFFFF"}),t[15]=P):P=t[15];let w;t[16]!==o?(w=Y.jsx(ll,{className:sn.songMediaButton,onClick:o,children:P}),t[16]=o,t[17]=w):w=t[17];let F;t[18]!==m||t[19]!==b||t[20]!==e?(F=e&&Y.jsx(ll,{className:sn.songMediaButton,onClick:m,children:b()}),t[18]=m,t[19]=b,t[20]=e,t[21]=F):F=t[21];let E;return t[22]!==F||t[23]!==C||t[24]!==S||t[25]!==R||t[26]!==w?(E=Y.jsxs(Y.Fragment,{children:[C,S,R,w,F]}),t[22]=F,t[23]=C,t[24]=S,t[25]=R,t[26]=w,t[27]=E):E=t[27],E},N8=n=>{const t=Re.c(4),{isRemaining:e}=n,i=Tt(Dr),a=Tt(Wl),o=e?(i-a)/1e3:i/1e3;let c;t[0]!==o?(c=ub(o),t[0]=o,t[1]=c):c=t[1];const u=c;let f;return t[2]!==u?(f=Y.jsx(Y.Fragment,{children:u}),t[2]=u,t[3]=f):f=t[3],f},uD=()=>{const n=Re.c(4),t=Tt(Wl)/1e3;let e;n[0]!==t?(e=ub(t),n[0]=t,n[1]=e):e=n[1];const i=e;let a;return n[2]!==i?(a=Y.jsx(Y.Fragment,{children:i}),n[2]=i,n[3]=a):a=n[3],a},s3=globalThis.jotaiAtomCache.get("/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/react-full/src/components/PrebuiltLyricPlayer/index.tsx/manualSeekTriggerAtom",zt(null));s3.debugLabel="manualSeekTriggerAtom";const O8=Gl.memo(()=>{const n=Re.c(38),t=Tt(Wl),e=Tt(Dr),i=Tt(Fr),a=Tt(Db),o=Tt(hb).onEmit,c=Tt(G2).onEmit,u=Ju(s3),[f,d]=qm(wb),m=Tt(Ih),g=Tt(Dh),v=Tt(Bh),x=m||void 0,b=g||void 0,C=v||void 0;let T;n[0]!==x||n[1]!==b||n[2]!==C?(T={fontFamily:x,fontWeight:b,letterSpacing:C},n[0]=x,n[1]=b,n[2]=C,n[3]=T):T=n[3];const S=T;let L;n[4]!==c||n[5]!==u?(L=K=>{c?.(K),u({time:K,timestamp:Date.now()})},n[4]=c,n[5]=u,n[6]=L):L=n[6];const R=L;let P;n[7]!==R||n[8]!==t||n[9]!==i||n[10]!==e?(P=Y.jsx(cb,{isPlaying:i,min:0,max:t,value:e,onChange:R}),n[7]=R,n[8]=t,n[9]=i,n[10]=e,n[11]=P):P=n[11];let w;n[12]===Symbol.for("react.memo_cache_sentinel")?(w=Y.jsx(N8,{}),n[12]=w):w=n[12];let F;n[13]!==S?(F=Y.jsx("div",{style:S,children:w}),n[13]=S,n[14]=F):F=n[14];let E;n[15]!==a||n[16]!==o?(E=a&&Y.jsx(YO,{className:sn.qualityTag,isDolbyAtmos:a.isDolbyAtmos,tagText:a.tagText,tagIcon:a.tagIcon,onClick:o}),n[15]=a,n[16]=o,n[17]=E):E=n[17];let O;n[18]!==E?(O=Y.jsx("div",{children:Y.jsx(NE,{mode:"popLayout",children:E})}),n[18]=E,n[19]=O):O=n[19];let I;n[20]!==S?(I={...S,cursor:"pointer",userSelect:"none"},n[20]=S,n[21]=I):I=n[21];let k;n[22]!==d||n[23]!==f?(k=()=>d(!f),n[22]=d,n[23]=f,n[24]=k):k=n[24];let U;n[25]!==f?(U=f?Y.jsx(N8,{isRemaining:!0}):Y.jsx(uD,{}),n[25]=f,n[26]=U):U=n[26];let G;n[27]!==I||n[28]!==k||n[29]!==U?(G=Y.jsx("div",{style:I,onClick:k,children:U}),n[27]=I,n[28]=k,n[29]=U,n[30]=G):G=n[30];let J;n[31]!==G||n[32]!==F||n[33]!==O?(J=Y.jsxs("div",{className:sn.progressBarLabels,children:[F,O,G]}),n[31]=G,n[32]=F,n[33]=O,n[34]=J):J=n[34];let tt;return n[35]!==J||n[36]!==P?(tt=Y.jsxs("div",{children:[P,J]}),n[35]=J,n[36]=P,n[37]=tt):tt=n[37],tt});function hD(n){switch(n){case"tiny":return"max(max(2.5vh, 1.25vw), 10px)";case"extra-small":return"max(max(3vh, 1.5vw), 10px)";case"small":return"max(max(4vh, 2vw), 12px)";case"large":return"max(max(6vh, 3vw), 16px)";case"extra-large":return"max(max(7vh, 3.5vw), 18px)";case"huge":return"max(max(8vh, 4vw), 20px)";default:return"max(max(5vh, 2.5vw), 14px)"}}const fD=n=>{const t=Re.c(36),{alignPosition:e,alignAnchor:i,bottomLine:a,optimizeOptions:o}=n,c=q.useRef(null),u=Tt(Fr),f=Tt(Ga),d=Tt(Ph),m=Tt(Dr),g=Tt(Ih),v=Tt(Dh),x=Tt(Bh),b=Tt(W2),C=Tt(db).lyricPlayer,T=Tt(mb),S=Tt(pb),L=Tt(gb),R=Tt(vb),P=Tt(Y2),w=Tt($2),F=Tt(yb),E=Tt(j2).onEmit,O=Tt(fb).onEmit,I=Tt(s3);let k;if(t[0]!==w||t[1]!==F||t[2]!==P||t[3]!==f){if(k=Dm(f),!P)for(const W of k)W.translatedLyric="";if(!w)for(const W of k)W.romanLyric="";if(F)for(const W of k){const[st,ct]=[W.romanLyric,W.translatedLyric];W.translatedLyric=st,W.romanLyric=ct}t[0]=w,t[1]=F,t[2]=P,t[3]=f,t[4]=k}else k=t[4];const U=k;let G,J;t[5]!==I?(G=()=>{I&&c.current?.lyricPlayer?.setCurrentTime(I.time,!0)},J=[I],t[5]=I,t[6]=G,t[7]=J):(G=t[6],J=t[7]),q.useEffect(G,J);const tt=g||void 0,K=v||void 0,A=x||void 0;let B;t[8]!==b?(B=hD(b),t[8]=b,t[9]=B):B=t[9];let N;t[10]!==tt||t[11]!==K||t[12]!==A||t[13]!==B?(N={width:"100%",height:"100%",fontFamily:tt,fontWeight:K,letterSpacing:A,"--amll-lp-font-size":B},t[10]=tt,t[11]=K,t[12]=A,t[13]=B,t[14]=N):N=t[14];const H=N,V=!d;let M;t[15]!==E?(M=W=>{const st=W.line.getLine().startTime;c.current?.lyricPlayer?.setCurrentTime(st,!0),E?.(W,c.current)},t[15]=E,t[16]=M):M=t[16];let z;t[17]!==O?(z=W=>O?.(W,c.current),t[17]=O,t[18]=z):z=t[18];let j;return t[19]!==i||t[20]!==e||t[21]!==a||t[22]!==T||t[23]!==S||t[24]!==L||t[25]!==C||t[26]!==R||t[27]!==u||t[28]!==m||t[29]!==o||t[30]!==U||t[31]!==M||t[32]!==z||t[33]!==H||t[34]!==V?(j=Y.jsx(FO,{style:H,ref:c,playing:u,disabled:V,alignPosition:e,alignAnchor:i,currentTime:m,lyricLines:U,optimizeOptions:o,enableBlur:T,enableScale:S,enableSpring:L,wordFadeWidth:R,lyricPlayer:C,onLyricLineClick:M,onLyricLineContextMenu:z,bottomLine:a}),t[19]=i,t[20]=e,t[21]=a,t[22]=T,t[23]=S,t[24]=L,t[25]=C,t[26]=R,t[27]=u,t[28]=m,t[29]=o,t[30]=U,t[31]=M,t[32]=z,t[33]=H,t[34]=V,t[35]=j):j=t[35],j},I8=n=>{const t=Re.c(5),{style:e,className:i}=n,a=Tt(i3),o=Tt(H2).onEmit;if(Tt(Cb)){let c;return t[0]!==i||t[1]!==a||t[2]!==o||t[3]!==e?(c=Y.jsx(BI,{value:a,min:0,max:1,style:e,className:i,onChange:o}),t[0]=i,t[1]=a,t[2]=o,t[3]=e,t[4]=c):c=t[4],c}return null},D8=n=>{const t=Re.c(17);let e,i,a;t[0]!==n?({className:e,showOtherButtons:a,...i}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a):(e=t[1],i=t[2],a=t[3]);const o=Tt(Z2),c=Tt(Bb);let u;t[4]!==e?(u=on(sn.controls,e),t[4]=e,t[5]=u):u=t[5];let f;t[6]!==o||t[7]!==a?(f=o==="controls"&&Y.jsx(cD,{showOtherButtons:a}),t[6]=o,t[7]=a,t[8]=f):f=t[8];let d;t[9]!==c||t[10]!==o?(d=o==="fft"&&Y.jsx(VO,{style:{width:"100%",height:"8vh"},fftData:c}),t[9]=c,t[10]=o,t[11]=d):d=t[11];let m;return t[12]!==i||t[13]!==u||t[14]!==f||t[15]!==d?(m=Y.jsxs("div",{className:u,...i,children:[f,d]}),t[12]=i,t[13]=u,t[14]=f,t[15]=d,t[16]=m):m=t[16],m},dD=n=>{const t=Re.c(67);let e,i,a,o;t[0]!==n?({className:i,bottomLineSlot:e,optimizeOptions:a,...o}=n,t[0]=n,t[1]=e,t[2]=i,t[3]=a,t[4]=o):(e=t[1],i=t[2],a=t[3],o=t[4]);const[c,u]=qm(J2),f=Tt(n3),d=Tt(Ib),m=Tt(Fr),g=Tt(Pb),v=Tt(Ph),x=Tt(_b),b=Tt(bb),C=Tt(Eb),T=Tt(Ab),S=Tt(F2).onEmit,[L,R]=q.useState(!1),[P,w]=q.useState(.25),[F,E]=q.useState("top"),O=q.useRef(null),[I,k]=q.useState(null),U=Tt($l),G=Tt(Sb),J=Tt(Q2);let tt,K;t[5]!==L||t[6]!==I?(tt=()=>{if(!L&&O.current&&I){const ie=new ResizeObserver(()=>{if(!(O.current&&I))return;const De=O.current.getBoundingClientRect(),Sn=I.getBoundingClientRect();w((De.top+De.height/2-Sn.top)/Sn.height)});return ie.observe(O.current),ie.observe(I),E("center"),()=>ie.disconnect()}L&&(w(.1),E("top"))},K=[L,I],t[5]=L,t[6]=I,t[7]=tt,t[8]=K):(tt=t[7],K=t[8]),q.useLayoutEffect(tt,K);const A=c&&(b==="auto"?d&&L:b==="force-immersive");let B;t[9]!==i?(B=on(sn.autoLyricLayout,i),t[9]=i,t[10]=B):B=t[10];const N=!m&&!d&&A;let H;t[11]!==f||t[12]!==d||t[13]!==N?(H=Y.jsx(ZO,{coverUrl:f,coverIsVideo:d,ref:O,musicPaused:N}),t[11]=f,t[12]=d,t[13]=N,t[14]=H):H=t[14];let V;t[15]!==S?(V=Y.jsx(WO,{onClick:S}),t[15]=S,t[16]=V):V=t[16];const M=c&&sn.hideLyric;let z;t[17]!==M?(z=on(sn.smallMusicInfo,M),t[17]=M,t[18]=z):z=t[18];let j;t[19]!==z?(j=Y.jsx(U0,{className:z}),t[19]=z,t[20]=j):j=t[20];let W;t[21]!==U.renderer||t[22]!==J||t[23]!==v||t[24]!==g||t[25]!==x||t[26]!==T||t[27]!==C||t[28]!==f||t[29]!==d?(W=typeof U.renderer=="string"&&U.renderer==="css-bg"?Y.jsx("div",{style:{zIndex:-1,width:"100%",height:"100%",minWidth:"0",minHeight:"0",overflow:"hidden",background:J}}):Y.jsx(PO,{album:f,albumIsVideo:d,lowFreqVolume:g,renderScale:T,fps:x,renderer:typeof U.renderer=="string"?U.renderer==="pixi"?ib:Oh:U.renderer,staticMode:C||!v,style:{zIndex:-1}}),t[21]=U.renderer,t[22]=J,t[23]=v,t[24]=g,t[25]=x,t[26]=T,t[27]=C,t[28]=f,t[29]=d,t[30]=W):W=t[30];const st=c&&sn.hideLyric;let ct;t[31]!==st?(ct=on(sn.bigMusicInfo,st),t[31]=st,t[32]=ct):ct=t[32];let ft;t[33]!==ct?(ft=Y.jsx(U0,{className:ct}),t[33]=ct,t[34]=ft):ft=t[34];let pt,mt;t[35]===Symbol.for("react.memo_cache_sentinel")?(pt=Y.jsx(O8,{}),mt=Y.jsx(D8,{className:sn.bigControls}),t[35]=pt,t[36]=mt):(pt=t[35],mt=t[36]);let dt;t[37]!==c||t[38]!==u||t[39]!==G?(dt=G&&Y.jsxs("div",{style:{display:"flex",justifyContent:"space-evenly"},children:[Y.jsx(Ma,{type:"lyrics",checked:!c,onClick:()=>u(!c)}),Y.jsx(Ma,{type:"airplay"}),Y.jsx(Ma,{type:"playlist"})]}),t[37]=c,t[38]=u,t[39]=G,t[40]=dt):dt=t[40];let St;t[41]===Symbol.for("react.memo_cache_sentinel")?(St=Y.jsx(I8,{className:sn.bigVolumeControl}),t[41]=St):St=t[41];let bt;t[42]!==ft||t[43]!==dt?(bt=Y.jsxs(Y.Fragment,{children:[ft,pt,mt,dt,St]}),t[42]=ft,t[43]=dt,t[44]=bt):bt=t[44];let Lt;t[45]===Symbol.for("react.memo_cache_sentinel")?(Lt=Y.jsxs(Y.Fragment,{children:[Y.jsx(U0,{className:sn.horizontalControls}),Y.jsx(O8,{}),Y.jsx(D8,{className:sn.controls,showOtherButtons:!0}),Y.jsx(I8,{})]}),t[45]=Lt):Lt=t[45];let wt;t[46]!==c||t[47]!==u||t[48]!==G?(wt=G&&Y.jsxs(Y.Fragment,{children:[Y.jsx(Ma,{type:"playlist"}),Y.jsx(Ma,{type:"lyrics",checked:!c,onClick:()=>u(!c)}),Y.jsx("div",{style:{flex:"1"}}),Y.jsx(Ma,{type:"airplay"})]}),t[46]=c,t[47]=u,t[48]=G,t[49]=wt):wt=t[49];let yt;t[50]!==F||t[51]!==P||t[52]!==e||t[53]!==a?(yt=Y.jsx(fD,{alignPosition:P,alignAnchor:F,bottomLine:e,optimizeOptions:a}),t[50]=F,t[51]=P,t[52]=e,t[53]=a,t[54]=yt):yt=t[54];let xt;return t[55]!==c||t[56]!==o||t[57]!==W||t[58]!==bt||t[59]!==wt||t[60]!==yt||t[61]!==B||t[62]!==H||t[63]!==V||t[64]!==j||t[65]!==A?(xt=Y.jsx(PE,{children:Y.jsx(aI,{onElementMounted:k,className:B,onLayoutChange:R,verticalImmerseCover:A,coverSlot:H,thumbSlot:V,smallControlsSlot:j,backgroundSlot:W,bigControlsSlot:bt,controlsSlot:Lt,horizontalBottomControls:wt,lyricSlot:yt,hideLyric:c,...o})}),t[55]=c,t[56]=o,t[57]=W,t[58]=bt,t[59]=wt,t[60]=yt,t[61]=B,t[62]=H,t[63]=V,t[64]=j,t[65]=A,t[66]=xt):xt=t[66],xt};function mD(n){return n.name}globalThis.jotaiAtomCache=globalThis.jotaiAtomCache||{cache:new Map,get(n,t){return this.cache.has(n)?this.cache.get(n):(this.cache.set(n,t),t)}};function no(n){let t=n.length;for(;--t>=0;)n[t]=0}const pD=3,gD=258,Fb=29,yD=256,vD=yD+1+Fb,Ub=30,bD=512,xD=new Array((vD+2)*2);no(xD);const TD=new Array(Ub*2);no(TD);const CD=new Array(bD);no(CD);const SD=new Array(gD-pD+1);no(SD);const _D=new Array(Fb);no(_D);const AD=new Array(Ub);no(AD);try{String.fromCharCode.apply(null,new Uint8Array(1))}catch{}const Um=new Uint8Array(256);for(let n=0;n<256;n++)Um[n]=n>=252?6:n>=248?5:n>=240?4:n>=224?3:n>=192?2:1;Um[254]=Um[255]=1;const Jt={TT:"http://www.w3.org/ns/ttml",TTM:"http://www.w3.org/ns/ttml#metadata",ITUNES:"http://music.apple.com/lyric-ttml-internal",AMLL:"http://www.example.com/ns/amll",XML:"http://www.w3.org/XML/1998/namespace",TTS:"http://www.w3.org/ns/ttml#styling"},Ie={Head:"head",Body:"body",Div:"div",P:"p",Title:"title",Name:"name",Meta:"meta",ITunesMetadata:"iTunesMetadata",Songwriters:"songwriters",Songwriter:"songwriter",Translation:"translation",Translations:"translations",Transliteration:"transliteration",Transliterations:"transliterations",Text:"text",ParserError:"parsererror",Agent:"agent"},$t={Timing:"timing",Id:"id",Key:"key",Value:"value",Lang:"lang",For:"for",SongPart:"songPart",SongPartKebab:"song-part",Begin:"begin",End:"end",Role:"role",Type:"type",Ruby:"ruby",Obscene:"obscene",EmptyBeat:"empty-beat"},mr={AmllObscene:"amll:obscene",AmllEmptyBeat:"amll:empty-beat",TtsRuby:"tts:ruby"},Wt={Word:"Word",Line:"Line",MimeXML:"application/xml",MusicName:"musicName",Artists:"artists",Album:"album",ISRC:"isrc",TTMLAuthorGithub:"ttmlAuthorGithub",TTMLAuthorGithubLogin:"ttmlAuthorGithubLogin",NCMMusicId:"ncmMusicId",QQMusicId:"qqMusicId",SpotifyId:"spotifyId",AppleMusicId:"appleMusicId",RoleBg:"x-bg",RoleTranslation:"x-translation",RoleRoman:"x-roman",Group:"group",Other:"other",AgentDefault:"v1",RubyContainer:"container",RubyBase:"base",RubyTextContainer:"textContainer",RubyText:"text",TimingMode:"timingMode",Language:"language"};var ED=class Bs{domParser;static TIME_REGEX=/^(?:(?:(?\d+):)?(?\d+):)?(?\d+(?:\.\d+)?)$/;static LEADING_SPACE_REGEX=/^\s/;static TRAILING_SPACE_REGEX=/\s$/;static MULTI_SPACE_REGEX=/\s+/g;normalizeText(t,e=!0){if(!t)return"";const i=t.replace(Bs.MULTI_SPACE_REGEX," ");return e?i.trim():i}constructor(t){if(t?.domParser)this.domParser=t.domParser;else if(typeof DOMParser<"u")this.domParser=new DOMParser;else throw new Error("No DOMParser found. If you are running in Node.js, please inject a DOMParser (e.g., @xmldom/xmldom).")}static parse(t,e){return new Bs(e).parse(t)}parse(t){if(!t||typeof t!="string")throw new Error("TTMLParser: Input must be a valid XML string.");const e=this.domParser.parseFromString(t,Wt.MimeXML),{metadata:i,sidecar:a}=this.parseHead(e),o=e.getElementsByTagName(Ie.ParserError)[0];if(o)throw new Error(`TTMLParser: XML parsing error: ${o.textContent}`);const c={metadata:i,lines:[]},u=e.documentElement;if(u){const f=this.getAttr(u,Jt.XML,$t.Lang);f&&(c.metadata.language=f);const d=this.getAttr(u,Jt.ITUNES,$t.Timing);(d&&d===Wt.Word||d===Wt.Line)&&(c.metadata.timingMode=d)}return this.parseBody(e,c,a),c.metadata.timingMode??=this.inferTimingMode(c.lines),c.metadata.platformIds&&(c.metadata.platformIds=this.sortPlatformIds(c.metadata.platformIds)),c}inferTimingMode(t){return t.some(e=>(e.words?.length??0)>1||(e.backgroundVocal?.words?.length??0)>1)?"Word":"Line"}sortPlatformIds(t){const e=["ncmMusicId","qqMusicId","spotifyId","appleMusicId"],i={};for(const a of e)t[a]&&(i[a]=t[a]);for(const a of Object.keys(t))i[a]||(i[a]=t[a]);return i}parseHead(t){const e=t.getElementsByTagName(Ie.Head)[0],i={title:[],artist:[],album:[],isrc:[],authorIds:[],authorNames:[],songwriters:[],agents:{},rawProperties:{}},a={};return e?(this.parseTTMElements(e,i),this.parseAMLLMeta(e,i),this.parseiTunesExtensions(e,i,a),this.deduplicateMetadata(i),{metadata:i,sidecar:a}):{metadata:i,sidecar:a}}deduplicateMetadata(t){const e=i=>i?Array.from(new Set(i)):[];if(t.title=e(t.title),t.artist=e(t.artist),t.album=e(t.album),t.isrc=e(t.isrc),t.authorIds=e(t.authorIds),t.authorNames=e(t.authorNames),t.songwriters=e(t.songwriters),t.platformIds)for(const i of Object.keys(t.platformIds))t.platformIds[i]&&(t.platformIds[i]=e(t.platformIds[i]));if(t.rawProperties)for(const i of Object.keys(t.rawProperties))t.rawProperties[i]&&(t.rawProperties[i]=e(t.rawProperties[i]))}parseTTMElements(t,e){const i=t.getElementsByTagNameNS(Jt.TTM,Ie.Title);i.length>0&&i[0].textContent&&e.title?.push(i[0].textContent.trim());const a=Array.from(t.getElementsByTagNameNS(Jt.TTM,Ie.Agent));for(const o of a){const c=this.getAttr(o,Jt.XML,$t.Id);if(!c)continue;const u=this.getAttr(o,Jt.TTM,$t.Type,$t.Type),f=o.getElementsByTagNameNS(Jt.TTM,Ie.Name),d={id:c};if(u&&(d.type=u),f.length>0&&f[0].textContent){const m=f[0].textContent.trim();m.length>0&&(d.name=m)}e.agents??={},e.agents[c]=d}}parseAMLLMeta(t,e){const i=Array.from(t.getElementsByTagNameNS(Jt.AMLL,Ie.Meta)).filter(a=>this.getAttr(a,Jt.AMLL,$t.Key)&&this.getAttr(a,Jt.AMLL,$t.Value));for(const a of i){const o=this.getAttr(a,Jt.AMLL,$t.Key),c=this.getAttr(a,Jt.AMLL,$t.Value)?.trim();if(!(!o||!c))switch(o){case Wt.MusicName:e.title?.push(c);break;case Wt.Artists:e.artist?.push(c);break;case Wt.Album:e.album?.push(c);break;case Wt.ISRC:e.isrc?.push(c);break;case Wt.TTMLAuthorGithub:e.authorIds?.push(c);break;case Wt.TTMLAuthorGithubLogin:e.authorNames?.push(c);break;case Wt.NCMMusicId:case Wt.QQMusicId:case Wt.SpotifyId:case Wt.AppleMusicId:e.platformIds??={},(e.platformIds[o]??=[]).push(c);break;default:e.rawProperties??={},(e.rawProperties[o]??=[]).push(c);break}}}extractSubContent(t,e,i=!1){const a={},o=this.normalizeText(t.text),c=!i&&t.words&&t.words.length>0;if(o||c){const u={text:o};e&&(u.language=e),c&&(t.words?.length===1&&t.words?.[0].startTime===0&&t.words?.[0].endTime===0||(u.words=t.words)),a.main=u}if("backgroundVocal"in t&&t.backgroundVocal){const u=t.backgroundVocal,f=this.normalizeText(u.text),d=!i&&u.words&&u.words.length>0;if(f||d){const m={text:f};e&&(m.language=e),d&&(u.words?.length===1&&u.words?.[0].startTime===0&&u.words?.[0].endTime===0||(m.words=u.words)),a.bg=m}}return a}parseiTunesExtensions(t,e,i){const a=Array.from(t.getElementsByTagName(Ie.ITunesMetadata));if(a.length!==0)for(const o of a){const c=o.getElementsByTagName(Ie.Songwriters)[0];if(c){const f=Array.from(c.getElementsByTagName(Ie.Songwriter));for(const d of f){const m=d.textContent?.trim();m&&e.songwriters?.push(m)}}const u=(f,d,m)=>{const g=o.getElementsByTagName(f)[0];if(!g)return;const v=Array.from(g.getElementsByTagName(d));for(const x of v){const b=this.getAttr(x,Jt.XML,$t.Lang),C=Array.from(x.getElementsByTagName(Ie.Text));for(const T of C){const S=T.getAttribute($t.For),L=this.parseCommonContent(T);if(S){const R=this.extractSubContent(L,b,!1);if(i[S]??={},R.main&&(i[S][m]??=[]).push(R.main),R.bg){const P=m==="translations"?"bgTranslations":"bgRomanizations";(i[S][P]??=[]).push(R.bg)}}}}};u(Ie.Translations,Ie.Translation,"translations"),u(Ie.Transliterations,Ie.Transliteration,"romanizations")}}parseTime(t){if(!t)return 0;const e=t.trim();if(e.length===0)return 0;if(e.endsWith("s")){const a=Number(e.slice(0,-1));return Number.isNaN(a)?0:Math.round(a*1e3)}const i=e.match(Bs.TIME_REGEX);if(i?.groups){const{seconds:a,minutes:o,hours:c}=i.groups,u=Number(a),f=o?parseInt(o,10):0,d=c?parseInt(c,10):0;if(!Number.isNaN(u)&&!Number.isNaN(f)&&!Number.isNaN(d)){const m=d*3600+f*60+u;return Math.round(m*1e3)}}return 0}parseBody(t,e,i){const a=t.getElementsByTagName(Ie.Body)[0];if(!a)return;const o=Array.from(a.childNodes);let c=0;for(const u of o){if(u.nodeType!==1)continue;const f=u,d=f.localName||f.tagName.toLowerCase().split(":").pop();if(d===Ie.Div){c++;const m=this.getAttr(f,Jt.ITUNES,$t.SongPartKebab)||this.getAttr(f,Jt.ITUNES,$t.SongPart),g=f.getElementsByTagNameNS(Jt.TT,Ie.P),v=g.length>0?Array.from(g):Array.from(f.getElementsByTagName(Ie.P));for(const x of v)this.processLineElement(x,e.lines,i,m,c)}else d===Ie.P&&(c++,this.processLineElement(f,e.lines,i,void 0,c))}}processLineElement(t,e,i,a,o){const c=this.getAttr(t,Jt.ITUNES,$t.Key);if(!c)return;const u={id:c,...this.parseCommonContent(t)};a&&(u.songPart=a),o!==void 0&&(u.blockIndex=o);const f=this.getAttr(t,Jt.TTM,Ie.Agent);f&&(u.agentId=f);const d=i[c];d&&(d.translations&&(u.translations??=[]).push(...d.translations),d.romanizations&&(u.romanizations??=[]).push(...d.romanizations),d.bgTranslations&&u.backgroundVocal&&(u.backgroundVocal.translations??=[]).push(...d.bgTranslations),d.bgRomanizations&&u.backgroundVocal&&(u.backgroundVocal.romanizations??=[]).push(...d.bgRomanizations)),e.push(u)}parseCommonContent(t){const e=this.getAttr(t,Jt.XML,$t.Begin,$t.Begin),i=this.getAttr(t,Jt.XML,$t.End,$t.End),a=this.parseTime(e),o=this.parseTime(i),c=this.extractNodeState(t);c.backgroundVocal&&(c.bgTranslations.length>0&&(c.backgroundVocal.translations??=[]).push(...c.bgTranslations),c.bgRomanizations.length>0&&(c.backgroundVocal.romanizations??=[]).push(...c.bgRomanizations)),this.finalizeWords(c.words);const{startTime:u,endTime:f}=this.calculateTimeRange(a,o,c.words,c.backgroundVocal),d=this.normalizeText(c.fullText),m=e!==null||i!==null;return this.applyFallbackWord(c.words,d,m,a,o,u,f),this.buildLyricBase(c,d,u,f)}extractNodeState(t){const e={fullText:"",words:[],translations:[],romanizations:[],bgTranslations:[],bgRomanizations:[],backgroundVocal:void 0},i=Array.from(t.childNodes);for(const a of i)a.nodeType===3?this.processTextNode(e,a):a.nodeType===1&&this.processElementNode(e,a);return e}calculateTimeRange(t,e,i,a){let o=t,c=e;const u=[...i];if(a&&u.push(a),u.length>0){let f=1/0,d=0;for(const m of u)m.startTimed&&(d=m.endTime);(o===0||f>0&&fc)&&(c=d)}return{startTime:o,endTime:c}}applyFallbackWord(t,e,i,a,o,c,u){t.length===0&&e.length>0&&i&&t.push({text:e,startTime:a>0?a:c,endTime:o>0?o:u,endsWithSpace:!1})}buildLyricBase(t,e,i,a){return{text:e,startTime:i,endTime:a,words:t.words.length>0?t.words:void 0,translations:t.translations.length>0?t.translations:void 0,romanizations:t.romanizations.length>0?t.romanizations:void 0,backgroundVocal:t.backgroundVocal}}processTextNode(t,e){const i=e.textContent||"",a=i.includes(` +`);if(a&&i.trim().length===0)return;const o=this.normalizeText(i,!1);t.fullText+=o,!a&&o.length>0&&o.trim().length===0&&t.words.length>0&&(t.words[t.words.length-1].endsWithSpace=!0)}processElementNode(t,e){const i=this.getAttr(e,Jt.TTM,$t.Role);if(this.getAttr(e,Jt.TTS,$t.Ruby,mr.TtsRuby)===Wt.RubyContainer){this.processRubyElement(t,e);return}switch(i){case Wt.RoleBg:t.backgroundVocal=this.parseBackgroundVocal(e);break;case Wt.RoleTranslation:{const a=this.parseInlineSubContent(e);a&&(a.main&&t.translations.push(a.main),a.bg&&t.bgTranslations.push(a.bg));break}case Wt.RoleRoman:{const a=this.parseInlineSubContent(e);a&&(a.main&&t.romanizations.push(a.main),a.bg&&t.bgRomanizations.push(a.bg));break}default:this.processWordElement(t,e);break}}processRubyElement(t,e){const i=this.getAttr(e,Jt.AMLL,$t.Obscene,mr.AmllObscene)==="true",a=this.getAttr(e,Jt.AMLL,$t.EmptyBeat,mr.AmllEmptyBeat);let o;if(a){const v=parseInt(a,10);Number.isNaN(v)||(o=v)}let c="";const u=[],f=Array.from(e.childNodes);for(const v of f){if(v.nodeType!==1)continue;const x=v,b=this.getAttr(x,Jt.TTS,$t.Ruby,mr.TtsRuby);if(b===Wt.RubyBase)c=this.normalizeText(x.textContent,!1);else if(b===Wt.RubyTextContainer){const C=Array.from(x.childNodes);for(const T of C){if(T.nodeType!==1)continue;const S=T;if(this.getAttr(S,Jt.TTS,$t.Ruby,mr.TtsRuby)===Wt.RubyText){const L=this.getAttr(S,Jt.XML,$t.Begin,$t.Begin),R=this.getAttr(S,Jt.XML,$t.End,$t.End),P=this.normalizeText(S.textContent,!1).trim();P&&L&&R&&u.push({text:P,startTime:this.parseTime(L),endTime:this.parseTime(R)})}}}}if(!c)return;t.fullText+=c;let d=0,m=0;u.length>0&&(d=Math.min(...u.map(v=>v.startTime)),m=Math.max(...u.map(v=>v.endTime)));const g=c.trim();if(g.length>0){const v=Bs.TRAILING_SPACE_REGEX.test(c);Bs.LEADING_SPACE_REGEX.test(c)&&t.words.length>0&&(t.words[t.words.length-1].endsWithSpace=!0),t.words.push({text:g,startTime:d,endTime:m,ruby:u.length>0?u:void 0,endsWithSpace:v,obscene:i?!0:void 0,emptyBeat:o})}}processWordElement(t,e){const i=this.getAttr(e,Jt.XML,$t.Begin,$t.Begin),a=this.getAttr(e,Jt.XML,$t.End,$t.End),o=this.getAttr(e,Jt.AMLL,$t.Obscene,mr.AmllObscene)==="true",c=this.getAttr(e,Jt.AMLL,$t.EmptyBeat,mr.AmllEmptyBeat);let u;if(c){const m=parseInt(c,10);Number.isNaN(m)||(u=m)}const f=e.textContent||"",d=this.normalizeText(f,!1);if(t.fullText+=d,i&&a){const m=f.includes(` +`);let g=!1,v=!1;m||(g=Bs.LEADING_SPACE_REGEX.test(d),v=Bs.TRAILING_SPACE_REGEX.test(d));const x=d.trim();g&&t.words.length>0&&(t.words[t.words.length-1].endsWithSpace=!0),x.length>0&&t.words.push({text:x,startTime:this.parseTime(i),endTime:this.parseTime(a),endsWithSpace:v,obscene:o?!0:void 0,emptyBeat:u})}}parseBackgroundVocal(t){const{backgroundVocal:e,...i}=this.parseCommonContent(t);if(i.text=i.text.replace(/^[((]+/,"").replace(/[))]+$/,""),i.words&&i.words.length>0){i.words[0].text=i.words[0].text.replace(/^[((]+/,"").trimStart();const a=i.words.length-1;i.words[a].text=i.words[a].text.replace(/[))]+$/,"").trimEnd()}return i}parseInlineSubContent(t){const e=this.getAttr(t,Jt.XML,$t.Lang),i=this.parseCommonContent(t),a=this.extractSubContent(i,e,!0);return a.main||a.bg?a:null}finalizeWords(t){if(t.length===0)return[];t[0].text=t[0].text.trimStart();const e=t.length-1;return t[e].text=t[e].text.trimEnd(),t[e].endsWithSpace=!1,t}getAttr(t,e,i,a){const o=t.getAttributeNS(e,i);if(o)return o;if(a){const c=t.getAttribute(a);if(c)return c}if(t.hasAttributes()){const c=Array.from(t.attributes);for(const u of c)if((u.localName||u.nodeName.split(":").pop())===i)return u.value}return null}};function wD(n,t){const e=[],i=(f,d,m)=>{let g=[];f.words&&f.words.length>0?g=f.words.map(C=>{const T={startTime:C.startTime,endTime:C.endTime,word:C.text+(C.endsWithSpace?" ":""),romanWord:"",obscene:C.obscene,emptyBeat:C.emptyBeat};return C.ruby&&C.ruby.length>0&&(T.ruby=C.ruby.map(S=>({startTime:S.startTime,endTime:S.endTime,word:S.text}))),T}):g=[{startTime:f.startTime,endTime:f.endTime,word:f.text,romanWord:""}];let v="";f.translations&&f.translations.length>0&&(v=f.translations[0].text);let x="",b;if(f.romanizations&&f.romanizations.length>0){const C=f.romanizations[0];b=C.words,(!b||b.length===0)&&(x=C.text)}return b&&g.length>0&&MD(g,b),{words:g,translatedLyric:v,romanLyric:x,isBG:d,isDuet:m,startTime:f.startTime,endTime:f.endTime}};let a=null,o=!1;for(const f of n.lines){const d=f.agentId||Wt.AgentDefault,m=n.metadata.agents?.[d],g=m?.type===Wt.Group,v=m?.type===Wt.Other;let x=!1;g?x=!1:a===null?(x=!!v,a=d,o=x):a===d?x=o:(x=!o,a=d,o=x);const b=i(f,!1,x);if(e.push(b),f.backgroundVocal){const C=i(f.backgroundVocal,!0,x);e.push(C)}}const c=[],u=n.metadata;if(u.title&&c.push([Wt.MusicName,u.title]),u.artist&&c.push([Wt.Artists,u.artist]),u.album&&c.push([Wt.Album,u.album]),u.isrc&&c.push([Wt.ISRC,u.isrc]),u.authorIds&&c.push([Wt.TTMLAuthorGithub,u.authorIds]),u.authorNames&&c.push([Wt.TTMLAuthorGithubLogin,u.authorNames]),u.language&&c.push([Wt.Language,[u.language]]),u.timingMode&&c.push([Wt.TimingMode,[u.timingMode]]),u.songwriters&&c.push([Ie.Songwriters,u.songwriters]),u.platformIds&&(u.platformIds.ncmMusicId&&c.push([Wt.NCMMusicId,u.platformIds.ncmMusicId]),u.platformIds.qqMusicId&&c.push([Wt.QQMusicId,u.platformIds.qqMusicId]),u.platformIds.spotifyId&&c.push([Wt.SpotifyId,u.platformIds.spotifyId]),u.platformIds.appleMusicId&&c.push([Wt.AppleMusicId,u.platformIds.appleMusicId])),u.rawProperties)for(const[f,d]of Object.entries(u.rawProperties))c.push([f,d]);return{lines:e,metadata:c}}function MD(n,t){let e=0;const i=.1,a=2;for(let o=0;o0){const T=Math.min(c.startTime,v.startTime),S=C/Math.max(1,Math.max(u,x)-T);S>f&&(f=S,d=g)}if(v.startTime>=u)break;g++}!m&&d!==-1&&f>=i&&(c.romanWord=t[d].text,e=d+1)}}function RD(n){return wD(ED.parse(n))}const LD=n=>({words:[],translatedLyric:"",romanLyric:"",isBG:!1,isDuet:!1,startTime:0,endTime:0,...n}),ND=n=>({startTime:0,endTime:0,word:"",...n}),OD=n=>Math.round(n.split(":").map(Number).reverse().reduce((t,e,i)=>t+e*60**i,0)*1e3),ID=60039999;function*DD(n){let t,e=!1;for(const i of n)e&&(yield[t,i]),t=i,e=!0}const B8=new TextEncoder().encode("!@#)(*$%"),P8=new TextEncoder().encode("123ZXC!@"),F8=new TextEncoder().encode("!@#)(NHL"),BD=[[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13],[15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,3,13,4,7,15,2,8,15,12,0,1,10,6,9,11,5,0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9],[10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12],[7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,3,15,0,6,10,10,13,8,9,4,5,11,12,7,2,14],[2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14,11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3],[12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13],[4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12],[13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]],PD=[16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25],U8=[32,1,2,3,4,5,4,5,6,7,8,9,8,9,10,11,12,13,12,13,14,15,16,17,16,17,18,19,20,21,20,21,22,23,24,25,24,25,26,27,28,29,28,29,30,31,32,1],FD=[1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1],UD=[56,48,40,32,24,16,8,0,57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35],kD=[62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,60,52,44,36,28,20,12,4,27,19,11,3],k8=[13,16,10,23,0,4,2,27,14,5,20,9,22,18,11,3,25,7,15,6,26,19,12,1,40,51,30,36,46,54,29,39,50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31];function z8(n,t){let e=0n,i=1n<>5,u=o&31,f=u>>3,d=u&7;n[c*4+3-f]>>7-d&1&&(e|=i),i>>=1n}return e}function V8(n,t){const e=4294967280n,i=n&e;return(i<>BigInt(28-t))&e}function Ra(n,t){const e=new Int32Array(32),i=z8(n,UD),a=z8(n,kD);let o=i<<4n,c=a<<4n;for(let u=0;u<16;u++){const f=FD[u];o=V8(o,f),c=V8(c,f);const d=t===1?15-u:u;let m=0n;for(let R=0;R>BigInt(31-P)&1n:c>>BigInt(31-(P-27))&1n)===1n&&(m|=1n<>40n&255n),v=Number(m>>32n&255n),x=Number(m>>24n&255n),b=g<<16|v<<8|x,C=Number(m>>16n&255n),T=Number(m>>8n&255n),S=Number(m&255n),L=C<<16|T<<8|S;e[d*2]=b,e[d*2+1]=L}return e}const zD=[34,42,50,58,2,10,18,26,36,44,52,60,4,12,20,28,38,46,54,62,6,14,22,30,40,48,56,64,8,16,24,32,33,41,49,57,1,9,17,25,35,43,51,59,3,11,19,27,37,45,53,61,5,13,21,29,39,47,55,63,7,15,23,31],VD=[37,5,45,13,53,21,61,29,38,6,46,14,54,22,62,30,39,7,47,15,55,23,63,31,40,8,48,16,56,24,64,32,33,1,41,9,49,17,57,25,34,2,42,10,50,18,58,26,35,3,43,11,51,19,59,27,36,4,44,12,52,20,60,28],kb=new Int32Array(2048),zb=new Int32Array(2048),km=new Int32Array(2048),zm=new Int32Array(2048);function GD(){const n=(t,e)=>{let i=0n;for(let a=0;a<64;a++){const o=e[a];t>>BigInt(64-o)&1n&&(i|=1n<>32n&4294967295n),zb[a]=Number(i&4294967295n)}for(let t=0;t<8;t++)for(let e=0;e<256;e++){const i=n(BigInt(e)<>32n&4294967295n),zm[a]=Number(i&4294967295n)}}GD();function jD(n){return n&32|(n&31)>>1|(n&1)<<4}function HD(n){let t=0;for(let e=0;e<32;e++){const i=PD[e],a=1<<31-e;(n&1<<32-i)!==0&&(t|=a)}return t}const Ji=new Int32Array(512);function qD(){for(let n=0;n<8;n++)for(let t=0;t<64;t++){const e=jD(t),i=BD[n][e]<<28-n*4;Ji[n<<6|t]=HD(i)}}qD();const bl=new Int32Array(1024),xl=new Int32Array(1024);function XD(){for(let n=0;n<4;n++){const t=(3-n)*8;for(let e=0;e<256;e++){let i=0,a=0;const o=e<>>32-U8[u]&1&&(i|=1<<23-u);for(let u=24;u<48;u++)o>>>32-U8[u]&1&&(a|=1<<47-u);const c=n<<8|e;bl[c]=i,xl[c]=a}}}XD();function G8(n,t,e){const i=n>>>24&255,a=n>>>16&255,o=n>>>8&255,c=n&255,u=bl[i]|bl[256|a]|bl[512|o]|bl[768|c],f=xl[i]|xl[256|a]|xl[512|o]|xl[768|c],d=u^t,m=f^e;return Ji[d>>>18&63]|Ji[64|d>>>12&63]|Ji[128|d>>>6&63]|Ji[192|d&63]|Ji[256|m>>>18&63]|Ji[320|m>>>12&63]|Ji[384|m>>>6&63]|Ji[448|m&63]}function La(n,t,e){let i=0,a=0;for(let u=0;u<8;u++){const f=u<<8|n[u];i|=kb[f],a|=zb[f]}for(let u=0;u<15;u++){const f=a;a=(i^G8(a,e[u*2],e[u*2+1]))>>>0,i=f}i=(i^G8(a,e[30],e[31]))>>>0;let o=0,c=0;for(let u=0;u<4;u++){const f=u<<8|i>>>24-u*8&255;o|=km[f],c|=zm[f];const d=u+4<<8|a>>>24-u*8&255;o|=km[d],c|=zm[d]}t[0]=o>>>24&255,t[1]=o>>>16&255,t[2]=o>>>8&255,t[3]=o&255,t[4]=c>>>24&255,t[5]=c>>>16&255,t[6]=c>>>8&255,t[7]=c&255}var YD=class{encryptSchedule;decryptSchedule;constructor(){this.decryptSchedule=[Ra(F8,1),Ra(P8,0),Ra(B8,1)],this.encryptSchedule=[Ra(B8,0),Ra(P8,1),Ra(F8,0)]}decryptBlock(n,t){const e=new Uint8Array(8),i=new Uint8Array(8);La(n,e,this.decryptSchedule[0]),La(e,i,this.decryptSchedule[1]),La(i,t,this.decryptSchedule[2])}encryptBlock(n,t){const e=new Uint8Array(8),i=new Uint8Array(8);La(n,e,this.encryptSchedule[0]),La(e,i,this.encryptSchedule[1]),La(i,t,this.encryptSchedule[2])}};new YD;function j8(n){const t=/^\[([a-z]+):([^\]]+)\]$/,e=/^\[((?:\d+:)*\d+(?:\.\d+)?)\](.*)$/,i=/^[((](.+)[))]$/,a=n.split(/\r?\n/).map(c=>c.trim()).filter(c=>c.length>0),o=[];for(let c of a){if(t.test(c))continue;const u=[];for(;;){const m=c.match(e);if(!m)break;const[,g,v]=m,x=OD(g);if(Number.isNaN(x))break;u.push(x),c=v}if(u.length===0)continue;c=c.trim();const f=c.match(i),d=!!f;f&&(c=f[1]);for(const m of u)o.push(LD({startTime:m,endTime:ID,words:[ND({word:c,startTime:m,endTime:m})],isBG:d}))}o.sort((c,u)=>c.startTime-u.startTime);for(const[c,u]of DD(o))c.endTime=c.words[0].endTime=u.startTime;return o.filter(c=>c.words[0].word)}function $D(n){return RD(n)}const Zt=Hm();for(let n=localStorage.length-1;n>=0;n-=1){const t=localStorage.key(n);t?.startsWith("amll-react-full.")&&localStorage.removeItem(t)}Zt.set(Ph,!0);Zt.set(Fr,!1);Zt.set(i3,1);function Vb(){Zt.set(Z2,tD.Controls),Zt.set(K2,!0),Zt.set(Y2,!0),Zt.set($2,!0),Zt.set(W2,eD.Large)}Vb();let yh=!1;function WD(){yh||(yh=!0,Zt.set($l,{renderer:Oh}))}function H8(){yh&&(yh=!1,Zt.set($l,{renderer:"css-bg"}))}Zt.set($l,{renderer:"css-bg"});Zt.set(Q2,"#141019");const Mi=n=>({onEmit:n}),Vm={listener:null};function hi(n,t){try{const e=window.amllHost;if(e&&typeof e.postMessage=="function"){e.postMessage(JSON.stringify({command:n,value:t}));return}}catch(e){console.warn("[amll-host] bridge failed",e)}console.log("[amll-host]",n,t??"")}Zt.set(k2,Mi(()=>hi("playPause")));Zt.set(G2,Mi(n=>hi("seek",n)));Zt.set(j2,Mi(n=>{hi("seek",n.line.getLine().startTime)}));Zt.set(H2,Mi(n=>hi("volume",n)));Zt.set(q2,Mi(()=>hi("shuffle")));Zt.set(X2,Mi(()=>hi("repeat")));Zt.set(V2,Mi(()=>hi("next")));Zt.set(z2,Mi(()=>hi("prev")));Zt.set(F2,Mi(()=>hi("close")));Zt.set(U2,Mi(()=>{Vm.listener?.()}));let Rl=0,Ll=performance.now(),r3=!1,Gb=0;const jb=()=>{if(r3){const n=Rl+(performance.now()-Ll);Zt.set(Dr,Math.round(n))}requestAnimationFrame(jb)};requestAnimationFrame(jb);function ZD(n){Gb=n;const t=performance.now();if(!r3){Rl=n,Ll=t,Zt.set(Dr,n);return}const e=Rl+(t-Ll);Math.abs(e-n)>250&&(Rl=n,Ll=t,Zt.set(Dr,n))}const Gm=208,KD=480;function QD(n){const e=Math.max(8,window.innerWidth-Gm-8),i=Math.max(8,window.innerHeight-280);return{x:Math.min(Math.max(n.x,8),e),y:Math.min(Math.max(n.y,8),i)}}function hl(n){return Y.jsx("button",{type:"button",className:"menu-item",disabled:n.disabled,onClick:n.onClick,children:n.label})}function JD(){const[n,t]=qm(J2),[e,i]=q.useState(null),a=q.useRef(null),o=q.useRef(null),c=q.useRef(0);q.useEffect(()=>{Vb()},[]);const u=q.useCallback(()=>{a.current!==null&&(window.clearTimeout(a.current),a.current=null)},[]),f=q.useCallback(v=>{c.current=Date.now(),i(QD(v))},[]);q.useEffect(()=>(Vm.listener=()=>f({x:window.innerWidth-Gm-16,y:64}),()=>{Vm.listener=null}),[f]);const d=v=>{if(v.touches.length!==1)return;const x=v.touches[0];o.current={x:x.clientX,y:x.clientY},u(),a.current=window.setTimeout(()=>{a.current=null,f({x:x.clientX,y:x.clientY})},KD)},m=v=>{const x=o.current;if(!x)return;const b=v.touches[0];(Math.abs(b.clientX-x.x)>10||Math.abs(b.clientY-x.y)>10)&&u()},g=()=>i(null);return Y.jsxs("div",{className:"stage",onTouchStart:d,onTouchMove:m,onTouchEnd:u,onTouchCancel:u,onContextMenu:v=>{v.preventDefault(),f({x:v.clientX,y:v.clientY})},children:[Y.jsx(dD,{id:"amll-lyric-player",style:{width:"100%",height:"100%"}}),e&&Y.jsx("div",{className:"menu-backdrop",onClick:()=>{Date.now()-c.current>300&&g()},children:Y.jsxs("div",{className:"menu-card",style:{left:e.x,top:e.y,width:Gm},onClick:v=>v.stopPropagation(),children:[Y.jsx(hl,{label:"上一首",disabled:!0}),Y.jsx(hl,{label:"暂停 / 继续",onClick:()=>{g(),hi("playPause")}}),Y.jsx(hl,{label:"下一首",disabled:!0}),Y.jsx("div",{className:"menu-sep"}),Y.jsx(hl,{label:n?"显示歌词":"隐藏歌词",onClick:()=>{t(!n),g()}}),Y.jsx("div",{className:"menu-sep"}),Y.jsx(hl,{label:"退出歌词页面",onClick:()=>{g(),hi("close")}})]})})]})}window.amllNative={setRawLyric(n,t,e){try{if(!n){Zt.set(Ga,[]);return}if(t==="ttml"){const a=$D(n).lines;console.log("[amll] parseTTML lines:",a.length),Zt.set(Ga,a);return}const i=j8(n);if(e){const a=new Map;for(const o of j8(e))a.set(Math.round(o.startTime),o.words[0]?.word??"");for(const o of i)o.translatedLyric=a.get(Math.round(o.startTime))??""}console.log("[amll] parseLrc lines:",i.length),Zt.set(Ga,i)}catch(i){console.warn("[amll] setRawLyric failed",i),Zt.set(Ga,[])}},setTime(n){ZD(n)},setPlaying(n){console.log("[amll] setPlaying:",n),r3=n,n&&(Rl=Gb,Ll=performance.now()),Zt.set(Fr,n)},setMeta(n,t,e){console.log("[amll] setMeta:",n,t,e),Zt.set(t3,n),Zt.set(e3,t.split(" / ").map(i=>i.trim()).filter(Boolean).map(i=>({id:i,name:i}))),Zt.set(Wl,e)},setCover(n){console.log("[amll] setCover:",n);const t=n||"";if(Zt.set(n3,t),!t){H8();return}const e=new Image;e.crossOrigin="anonymous",e.onload=()=>WD(),e.onerror=()=>H8(),e.src=t},setTheme(n){document.documentElement.classList.toggle("dark",n),document.body.style.background=n?"#0B0B0F":"#FBFBFD"},setInsets(n,t){const e=document.documentElement;e.style.setProperty("--amll-inset-top",`${Math.max(0,n)}px`),e.style.setProperty("--amll-inset-bottom",`${Math.max(0,t)}px`)}};class tB extends Gl.Component{constructor(){super(...arguments),this.state={error:null}}static getDerivedStateFromError(t){return{error:t}}componentDidCatch(t,e){console.error("[amll] render crashed:",t,e.componentStack)}render(){const{error:t}=this.state;return t?Y.jsxs("div",{className:"page-error",children:[Y.jsx("div",{className:"page-error-title",children:"歌词页渲染失败"}),Y.jsx("pre",{children:String(t.stack||t)})]}):this.props.children}}lC.createRoot(document.getElementById("root")).render(Y.jsx(UC,{store:Zt,children:Y.jsx(tB,{children:Y.jsx(JD,{})})})); diff --git a/android/app/src/main/assets/lyrics/index.html b/android/app/src/main/assets/lyrics/index.html new file mode 100644 index 0000000..056124e --- /dev/null +++ b/android/app/src/main/assets/lyrics/index.html @@ -0,0 +1,13 @@ + + + + + + AMLL Lyrics + + + + +
+ + diff --git a/android/app/src/main/java/dev/amll/hub/android/AmllHubApp.kt b/android/app/src/main/java/dev/amll/hub/android/AmllHubApp.kt index 02d5904..f95dcb2 100644 --- a/android/app/src/main/java/dev/amll/hub/android/AmllHubApp.kt +++ b/android/app/src/main/java/dev/amll/hub/android/AmllHubApp.kt @@ -1,7 +1,72 @@ package dev.amll.hub.android import android.app.Application +import android.os.Build +import android.util.Log +import androidx.core.content.pm.PackageInfoCompat import dagger.hilt.android.HiltAndroidApp +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale @HiltAndroidApp -class AmllHubApp : Application() +class AmllHubApp : Application() { + + override fun onCreate() { + super.onCreate() + installCrashLogger() + } + + /** + * 未捕获异常的 last-resort 记录。 + * + * 正式环境应接入 Crashlytics / Bugly / ACRA 做聚合与崩溃率统计; + * 这里只保留最小可用信息(版本 / 机型 / 系统 / 堆栈),并对凭证做脱敏、限制文件大小, + * 供没有 adb 时排查问题。 + */ + private fun installCrashLogger() { + val previous = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { thread, error -> + runCatching { + File(filesDir, CRASH_FILE).writeText(buildReport(thread, error)) + } + // 交回系统默认处理器,保证进程照常退出 + previous?.uncaughtException(thread, error) + } + } + + private fun buildReport(thread: Thread, error: Throwable): String { + val packageInfo = runCatching { + packageManager.getPackageInfo(packageName, 0) + }.getOrNull() + val raw = buildString { + appendLine("time: ${LocalDateTime.now().format(STAMP_FORMAT)}") + appendLine( + "app: ${packageInfo?.versionName ?: "?"} " + + "(${packageInfo?.let { PackageInfoCompat.getLongVersionCode(it) } ?: 0})", + ) + appendLine("device: ${Build.MANUFACTURER} ${Build.MODEL}") + appendLine("os: Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") + appendLine("abi: ${Build.SUPPORTED_ABIS.joinToString()}") + appendLine("thread: ${thread.name}") + appendLine(Log.getStackTraceString(error)) + } + // 堆栈里可能带出请求头、token、cookie,落盘前先抹掉 + return SECRET_PATTERN.replace(raw) { "${it.groupValues[1]}=" } + .take(MAX_REPORT_CHARS) + } + + companion object { + const val CRASH_FILE = "last_crash.txt" + + /** 报告上限 64 KB,避免异常循环反复写入撑爆 filesDir */ + private const val MAX_REPORT_CHARS = 64 * 1024 + + private val STAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US) + + private val SECRET_PATTERN = Regex( + "(?i)\\b(token|access_token|refresh_token|authorization|cookie|password|secret)\\b\\s*[=:]\\s*\\S+", + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/MainActivity.kt b/android/app/src/main/java/dev/amll/hub/android/MainActivity.kt index 52c4778..2acbfdf 100644 --- a/android/app/src/main/java/dev/amll/hub/android/MainActivity.kt +++ b/android/app/src/main/java/dev/amll/hub/android/MainActivity.kt @@ -1,20 +1,42 @@ package dev.amll.hub.android +import android.Manifest +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint +import dev.amll.hub.android.data.QrTicketParser import dev.amll.hub.android.nav.AmllNavGraph import dev.amll.hub.android.ui.theme.AmllHubTheme +import java.io.File @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -22,10 +44,22 @@ class MainActivity : ComponentActivity() { /** 扫码登录 deep link 携带的 ticket,null 表示无待处理请求 */ private var pendingTicket by mutableStateOf(null) + /** 上次崩溃的堆栈,非空时启动后弹窗显示 */ + private var crashReport by mutableStateOf(null) + + /** Android 13+ 媒体通知需要运行时授权,否则系统播控看不到 */ + private val notificationPermission = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { /* 拒绝也不影响播放,只是没有通知栏 */ } + override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() + // 必须在 super.onCreate() 之前接管系统启动页,冷启动不再先闪白屏 + installSplashScreen() super.onCreate(savedInstanceState) + enableEdgeToEdge() pendingTicket = extractTicket(intent) + crashReport = readCrashReport() + requestNotificationPermission() setContent { AmllHubTheme { @@ -37,11 +71,30 @@ class MainActivity : ComponentActivity() { pendingTicket = pendingTicket, onTicketConsumed = { pendingTicket = null }, ) + crashReport?.let { text -> + CrashReportDialog(text) { + crashReport = null + File(filesDir, AmllHubApp.CRASH_FILE).delete() + } + } } } } } + private fun readCrashReport(): String? = runCatching { + File(filesDir, AmllHubApp.CRASH_FILE).takeIf { it.exists() }?.readText()?.takeIf { it.isNotBlank() } + }.getOrNull() + + private fun requestNotificationPermission() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + val granted = ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + if (!granted) notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } + /** 应用已在后台时再次扫码,走这里 */ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -50,11 +103,38 @@ class MainActivity : ComponentActivity() { } /** 从 amllhub://qrlogin?ticket=xxx 中取出 ticket */ - private fun extractTicket(intent: Intent?): String? { - val data = intent?.data ?: return null - if (data.scheme != "amllhub") return null - return data.getQueryParameter("ticket")?.takeIf { it.isNotBlank() } - ?: data.lastPathSegment?.takeIf { it.isNotBlank() } - } + private fun extractTicket(intent: Intent?): String? = + QrTicketParser.parse(intent?.dataString.orEmpty()).takeIf { it.isNotBlank() } } +/** 崩溃堆栈弹窗:可长按选中,也可以一键复制 */ +@Composable +private fun CrashReportDialog(text: String, onDismiss: () -> Unit) { + val context = LocalContext.current + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("上次崩溃的堆栈") }, + text = { + SelectionContainer { + Text( + text, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()), + ) + } + }, + confirmButton = { + TextButton(onClick = { + runCatching { + context.getSystemService(Context.CLIPBOARD_SERVICE) + .let { it as? ClipboardManager } + ?.setPrimaryClip(ClipData.newPlainText("crash", text)) + } + onDismiss() + }) { Text("复制并关闭") } + }, + ) +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/QrTicketParser.kt b/android/app/src/main/java/dev/amll/hub/android/data/QrTicketParser.kt new file mode 100644 index 0000000..fc01749 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/QrTicketParser.kt @@ -0,0 +1,20 @@ +package dev.amll.hub.android.data + +/** + * 扫码内容解析:兼容 `amllhub://qrlogin?ticket=xxx` 与裸 ticket。 + * 生产代码与单元测试共用这一份实现。 + */ +object QrTicketParser { + + fun parse(raw: String): String { + val trimmed = raw.trim() + if (trimmed.isEmpty()) return "" + val marker = "ticket=" + val index = trimmed.indexOf(marker) + return if (index >= 0) { + trimmed.substring(index + marker.length).substringBefore('&').trim() + } else { + trimmed + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/local/AuthStore.kt b/android/app/src/main/java/dev/amll/hub/android/data/local/AuthStore.kt index 9c18fe5..5a85eb9 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/local/AuthStore.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/local/AuthStore.kt @@ -4,39 +4,63 @@ import android.content.Context import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton private val Context.dataStore by preferencesDataStore(name = "amll_auth") -/** 登录态持久化:token + 用户资料,替代网页端的 localStorage */ +/** 登录态持久化:token(Keystore 加密)+ 用户资料,替代网页端的 localStorage */ @Singleton class AuthStore @Inject constructor( - private val context: Context, + @ApplicationContext private val context: Context, ) { private val tokenKey = stringPreferencesKey("amll_hub_token") private val userKey = stringPreferencesKey("amll_hub_user") + private val cipher = TokenCipher() /** 内存缓存:OkHttp 拦截器同步读取,不能挂起 */ @Volatile var cachedToken: String? = null private set - val tokenFlow: Flow = context.dataStore.data.map { it[tokenKey] } + val tokenFlow: Flow = context.dataStore.data.map { prefs -> + prefs[tokenKey]?.let { stored -> + // 历史明文数据(没有 v1: 前缀)直接透传,下次 save 时会被替换为密文 + if (stored.startsWith("v1:")) cipher.decrypt(stored) else stored + } + } + + init { + // 预热缓存:拦截器是同步的,冷启动后由这里把 DataStore 里的 token 搬到内存 + CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + tokenFlow.collect { cachedToken = it } + } + } suspend fun token(): String? { cachedToken?.let { return it } - val stored = context.dataStore.data.first()[tokenKey] - cachedToken = stored - return stored + val stored = context.dataStore.data.first()[tokenKey] ?: return null + val plain = if (stored.startsWith("v1:")) cipher.decrypt(stored) else stored + cachedToken = plain + // 旧明文迁移为密文;迁移失败也不影响本次登录态 + if (plain != null && !stored.startsWith("v1:")) { + runCatching { context.dataStore.edit { it[tokenKey] = cipher.encrypt(plain) } } + } + return plain } suspend fun save(token: String) { cachedToken = token - context.dataStore.edit { it[tokenKey] = token } + val encrypted = cipher.encrypt(token) + context.dataStore.edit { it[tokenKey] = encrypted } } suspend fun clear() { diff --git a/android/app/src/main/java/dev/amll/hub/android/data/local/TokenCipher.kt b/android/app/src/main/java/dev/amll/hub/android/data/local/TokenCipher.kt new file mode 100644 index 0000000..e5d18c7 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/local/TokenCipher.kt @@ -0,0 +1,67 @@ +package dev.amll.hub.android.data.local + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import android.util.Log +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +/** + * 用 Android Keystore 里的 AES-GCM 密钥加密登录 token。 + * + * 密文格式:`v1:` + Base64(IV(12B) + ciphertext)。 + * 密钥不出 Keystore,卸载重装后旧密文自然失效。 + */ +internal class TokenCipher { + + private val keyStore: KeyStore = KeyStore.getInstance(KEYSTORE).apply { load(null) } + + fun encrypt(plain: String): String { + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey()) + val iv = cipher.iv + val encrypted = cipher.doFinal(plain.toByteArray(Charsets.UTF_8)) + return PREFIX + Base64.encodeToString(iv + encrypted, Base64.NO_WRAP) + } + + /** 解密失败(密钥被清除、数据损坏、旧版本明文)时返回 null */ + fun decrypt(stored: String): String? = runCatching { + if (!stored.startsWith(PREFIX)) return@runCatching null + val raw = Base64.decode(stored.removePrefix(PREFIX), Base64.NO_WRAP) + if (raw.size <= IV_SIZE) return@runCatching null + val iv = raw.copyOfRange(0, IV_SIZE) + val payload = raw.copyOfRange(IV_SIZE, raw.size) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(TAG_BITS, iv)) + cipher.doFinal(payload).toString(Charsets.UTF_8).takeIf { it.isNotBlank() } + }.onFailure { + Log.w(TAG, "token decrypt failed", it) + }.getOrNull() + + private fun getOrCreateKey(): SecretKey { + (keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey?.let { return it } + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE) + generator.init( + KeyGenParameterSpec.Builder(ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build(), + ) + return generator.generateKey() + } + + private companion object { + const val TAG = "TokenCipher" + const val KEYSTORE = "AndroidKeyStore" + const val ALIAS = "amll_hub_token_key" + const val TRANSFORMATION = "AES/GCM/NoPadding" + const val PREFIX = "v1:" + const val IV_SIZE = 12 + const val TAG_BITS = 128 + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/ApiErrorParser.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/ApiErrorParser.kt new file mode 100644 index 0000000..ac131ad --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/ApiErrorParser.kt @@ -0,0 +1,41 @@ +package dev.amll.hub.android.data.remote + +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import retrofit2.HttpException +import java.io.IOException + +/** + * 统一解析后端 `{code,message,data}` 错误响应,并把网络层异常映射成可展示的异常。 + * ContentRepository / AuthRepository 共用同一套逻辑,避免两处行为漂移。 + */ +object ApiErrorParser { + + private val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + } + + /** 从错误响应体里取 message(转义、嵌套结构都交给 kotlinx.serialization) */ + fun message(e: HttpException): String? = runCatching { + e.response()?.errorBody()?.string() + ?.let { json.decodeFromString>(it).message } + ?.takeIf { it.isNotBlank() } + }.getOrNull() + + /** 把任意异常映射成带用户可读消息的异常;已是业务异常的原样返回 */ + fun map(e: Throwable, fallbackPrefix: String = "请求失败"): Throwable = when (e) { + is HttpException -> { + val backendMessage = message(e) + if (e.code() == 401) { + if (backendMessage == null) UnauthorizedException() else UnauthorizedException(backendMessage) + } else { + ApiException(e.code(), backendMessage ?: "$fallbackPrefix(HTTP ${e.code()})") + } + } + + is SerializationException -> IOException("响应解析失败:${e.message.orEmpty().take(140)}") + is IOException -> NetworkException() + else -> e + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthApi.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthApi.kt index 6590315..cb6c5dd 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthApi.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthApi.kt @@ -17,6 +17,14 @@ interface AuthApi { @POST("api/v1/auth/send-code") suspend fun sendCode(@Body body: SendCodeRequest): ApiResponse + /** 发码前先确认账号存在,能给出「未注册」这类明确提示 */ + @POST("api/v1/auth/check-user") + suspend fun checkUser(@Body body: CheckUserRequest): ApiResponse + + /** 人机验证配置(网页端同样在切到验证码 tab 时拉取) */ + @GET("api/v1/auth/captcha") + suspend fun captcha(): ApiResponse + @GET("api/v1/auth/profile") suspend fun profile(): ApiResponse diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthInterceptor.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthInterceptor.kt index fa8d4a8..b18482b 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthInterceptor.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthInterceptor.kt @@ -1,7 +1,6 @@ package dev.amll.hub.android.data.remote import dev.amll.hub.android.data.local.AuthStore -import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject @@ -24,8 +23,8 @@ class AuthInterceptor @Inject constructor( return chain.proceed(request) } - // OkHttp 拦截器是同步的,这里用 runBlocking 读内存缓存(正常已有值,不会真正阻塞) - val token = authStore.cachedToken ?: runBlocking { authStore.token() } + // OkHttp 拦截器是同步的:只读 AuthStore 的内存缓存,不阻塞网络线程 + val token = authStore.cachedToken val newRequest = if (token.isNullOrBlank()) { request diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt index 6aa67f6..bfea7b5 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/AuthRepository.kt @@ -3,15 +3,11 @@ package dev.amll.hub.android.data.remote import dev.amll.hub.android.data.local.AuthStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import retrofit2.HttpException import java.io.IOException import javax.inject.Inject import javax.inject.Singleton -/** 解析错误响应体用的轻量 Json */ -private val errorJson = Json { ignoreUnknownKeys = true } - /** * 认证仓库:统一处理后端 {code, message, data} 包装与错误映射。 * 对应网页端 lib/api.ts 的 request() 管道。 @@ -34,10 +30,33 @@ class AuthRepository @Inject constructor( return resp.also { authStore.save(it.token) } } - suspend fun sendCode(checkType: String, dest: String) { - request { api.sendCode(SendCodeRequest(checkType = checkType, dest = dest.trim())) } + suspend fun sendCode( + checkType: String, + dest: String, + captchaType: String = "", + captchaToken: String = "", + ) { + request { + api.sendCode( + SendCodeRequest( + checkType = checkType, + dest = dest.trim(), + captchaType = captchaType, + captchaToken = captchaToken, + ), + ) + } + } + + /** 账号预检:不存在时后端返回 404「该手机号/邮箱未注册」 */ + suspend fun checkUser(checkType: String, dest: String) { + request { api.checkUser(CheckUserRequest(checkType, dest.trim())) } } + /** 人机验证配置;接口异常时按「无需验证」降级,与网页端一致 */ + suspend fun captchaConfig(): CaptchaConfig = + runCatching { request { api.captcha() } }.getOrNull() ?: CaptchaConfig() + suspend fun profile(): UserProfile = call { api.profile() } suspend fun currentToken(): String? = authStore.token() @@ -81,7 +100,7 @@ class AuthRepository @Inject constructor( val resp = try { block() } catch (e: HttpException) { - val backendMsg = errorMessage(e) + val backendMsg = ApiErrorParser.message(e) if (e.code() == 401) { // 401 既可能是 token 失效,也可能是「用户名或密码错误」, // 用后端文案覆盖默认提示;类型仍是 UnauthorizedException。 @@ -98,11 +117,4 @@ class AuthRepository @Inject constructor( } resp.data } - - /** 后端错误响应体形如 {"code":400,"message":"验证码错误"},取出 message 供 UI 展示 */ - private fun errorMessage(e: HttpException): String? = runCatching { - e.response()?.errorBody()?.string() - ?.let { errorJson.decodeFromString>(it).message } - ?.takeIf { it.isNotBlank() } - }.getOrNull() } diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentApi.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentApi.kt new file mode 100644 index 0000000..f2ed958 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentApi.kt @@ -0,0 +1,176 @@ +package dev.amll.hub.android.data.remote + +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * 内容相关接口,路径与 frontend/src/lib/api.ts 保持一致。 + */ +interface ContentApi { + + @GET("api/v1/stats") + suspend fun stats(): ApiResponse + + @GET("api/v1/latest-songs") + suspend fun latestSongs(): ApiResponse> + + @GET("api/v1/daily-recommendations") + suspend fun dailyRecommendations(): ApiResponse> + + @GET("api/v1/daily-recommendations/today") + suspend fun todayRecommendation(): ApiResponse + + @GET("api/v1/daily-recommendations/like/{id}") + suspend fun dailyLikeStatus(@Path("id") id: Long): ApiResponse + + @POST("api/v1/daily-recommendations/like/{id}") + suspend fun toggleDailyLike(@Path("id") id: Long): ApiResponse + + @GET("api/v1/not-found-ranking") + suspend fun notFoundRanking( + @Query("limit") limit: String = "all", + @Query("days") days: Int = 7, + @Query("platform") platform: String? = null, + ): ApiResponse + + @GET("api/v1/notifications") + suspend fun notifications( + @Query("page") page: Int = 1, + @Query("limit") limit: Int = 20, + @Query("type") type: String? = null, + ): ApiResponse + + @POST("api/v1/notifications/read-all") + suspend fun markAllNotificationsRead(): ApiResponse + + @POST("api/v1/notifications/{id}/read") + suspend fun markNotificationRead(@Path("id") id: Long): ApiResponse + + @GET("api/v1/submissions") + suspend fun submissions( + @Query("page") page: Int = 1, + @Query("limit") limit: Int = 50, + @Query("status") status: String = "all", + @Query("mode") mode: String = "creator", + ): ApiResponse + + /** 投稿统计(创作中心顶部卡片) */ + @GET("api/v1/submissions/stats") + suspend fun submissionStats(@Query("mode") mode: String = "creator"): ApiResponse + + @GET("api/v1/submissions/{id}") + suspend fun submission(@Path("id") id: Long): ApiResponse + + @GET("api/v1/search") + suspend fun search( + @Query("q") keyword: String, + @Query("field") field: String = "all", + @Query("limit") limit: Int = 20, + ): ApiResponse + + @GET("api/v1/ncm/search") + suspend fun ncmSearch( + @Query("q") keyword: String, + @Query("limit") limit: Int = 20, + ): ApiResponse + + /** 网易云单曲解析:拿到音频直链 + 封面 / 歌手 / 歌词 */ + @GET("api/v1/ncm/parse-music") + suspend fun ncmParseMusic( + @Query("songId") songId: String, + @Query("level") level: String = "exhigh", + ): ApiResponse + + /** 网易云歌单解析 */ + @GET("api/v1/ncm/parse-playlist") + suspend fun ncmParsePlaylist( + @Query("playlistId") playlistId: String, + ): ApiResponse + + /** 每日推荐投稿列表(创作中心) */ + @GET("api/v1/daily-recommendations/submissions") + suspend fun myDailySubmissions(): ApiResponse + + /** 搜索 IP 投稿列表(创作中心) */ + @GET("api/v1/search-ip/submissions") + suspend fun mySearchIpSubmissions(): ApiResponse + + /* ===== 投稿流程(网页端 LyricSubmitForm / DailyRecommendForm 同款接口) ===== */ + + /** TTML 校验:body 是原始 TTML 文本(text/plain) */ + @POST("api/v1/submissions/validate") + suspend fun validateTtml(@Body body: RequestBody): ApiResponse + + /** TTML 上传到待审核区:multipart file + fileName */ + @Multipart + @POST("api/v1/uploads/ttml") + suspend fun uploadTtml( + @Part file: MultipartBody.Part, + @Part("fileName") fileName: RequestBody, + ): ApiResponse + + /** 创建投稿(字幕文件 + 元数据),返回 { id } */ + @POST("api/v1/submissions") + suspend fun createSubmission(@Body body: CreateSubmissionRequest): ApiResponse + + /** 投稿音源附件:multipart audio + submissionId */ + @Multipart + @POST("api/v1/uploads/audio") + suspend fun uploadAudio( + @Part audio: MultipartBody.Part, + @Part("submissionId") submissionId: RequestBody, + ): Response + + /** 每日推荐封面:上传到临时区,返回 { tempKey } */ + @Multipart + @POST("api/v1/daily-recommendations/upload-temp") + suspend fun uploadDailyCover(@Part file: MultipartBody.Part): ApiResponse + + /** 每日推荐封面:由后端代理下载网易云封面并转存 */ + @POST("api/v1/daily-recommendations/upload-temp-from-url") + suspend fun uploadDailyCoverFromUrl(@Body body: UrlRequest): ApiResponse + + /** 每日推荐日期查重 */ + @GET("api/v1/daily-recommendations/check-date") + suspend fun checkDailyDate(@Query("date") date: String): ApiResponse + + /** 创建每日推荐投稿:multipart 表单 */ + @Multipart + @POST("api/v1/daily-recommendations") + suspend fun createDailyRecommendation( + @Part("songName") songName: RequestBody, + @Part("artist") artist: RequestBody, + @Part("coverTempKey") coverTempKey: RequestBody, + @Part("date") date: RequestBody, + @Part("comment") comment: RequestBody, + @Part("ncmId") ncmId: RequestBody, + ): ApiResponse + + /* ===== 稿件操作:只关心 HTTP 是否成功,不解析具体返回体 ===== */ + + @POST("api/v1/submissions/{id}/close") + suspend fun closeSubmission(@Path("id") id: Long): Response + + @POST("api/v1/submissions/{id}/revision/adopt") + suspend fun adoptRevision(@Path("id") id: Long): Response + + @POST("api/v1/submissions/{id}/revision/reject") + suspend fun rejectRevision(@Path("id") id: Long): Response + + @PUT("api/v1/auth/profile") + suspend fun updateProfile(@Body body: ProfileUpdateRequest): ApiResponse + + /** 个人活动统计(热力图) GET /api/v1/users/me/activity */ + @GET("api/v1/users/me/activity") + suspend fun activity(@Query("year") year: Int? = null): ApiResponse +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentDto.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentDto.kt new file mode 100644 index 0000000..469d84f --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/ContentDto.kt @@ -0,0 +1,455 @@ +package dev.amll.hub.android.data.remote + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull + +/** 词库统计 GET /api/v1/stats */ +@Serializable +data class StatsDto( + val totalSongs: Long = 0, + val totalArtists: Long = 0, + val totalAlbums: Long = 0, + val totalWords: Long = 0, + val totalLines: Long = 0, + val lastSyncAt: String = "", +) + +/** 最新收录 GET /api/v1/latest-songs */ +@Serializable +data class LatestSongDto( + val id: Long = 0, + val songId: Long = 0, + val ncmId: String = "", + val title: String = "", + val artist: String = "", + val coverUrl: String = "", +) + +/** 每日推荐 GET /api/v1/daily-recommendations */ +@Serializable +data class DailyRecDto( + val id: Long = 0, + val date: String = "", + val songName: String = "", + val artist: String = "", + val coverKey: String = "", + val ncmId: String = "", + val comment: String = "", + val submitter: String = "", + val likeCount: Int = 0, + val status: String = "", +) + +/** 无歌词排行 GET /api/v1/not-found-ranking */ +@Serializable +data class RankingItemDto( + val id: Long = 0, + val platform: String = "", + val platformId: String = "", + val songName: String = "", + val artists: List = emptyList(), + val cover: String = "", + val album: String = "", + val requestCount: Int = 0, + val lastSeenAt: String = "", +) + +@Serializable +data class RankingResultDto( + val total: Int = 0, + val days: Int = 7, + val items: List = emptyList(), +) + +/** 站内消息 GET /api/v1/notifications */ +@Serializable +data class MessageDto( + val id: Long = 0, + val type: String = "", + val title: String = "", + val content: String = "", + val read: Boolean = false, + val createdAt: String = "", +) + +@Serializable +data class NotificationListDto( + val items: List = emptyList(), + val total: Int = 0, + val unread: Int = 0, +) + +@Serializable +data class UnreadDto(val unread: Int = 0) + +/** 投稿列表 GET /api/v1/submissions — 后端字段是 title/artist,不是 songName */ +@Serializable +data class SubmissionDto( + val id: Long = 0, + val title: String = "", + val artist: String = "", + val album: String = "", + val status: String = "", + val language: String = "", + val notes: String = "", + val fileName: String = "", + val ncmId: String = "", + val submitter: String = "", + val reviewComment: String = "", + val hasRevision: Boolean = false, + val revisionFileName: String = "", + val createdAt: String = "", + val updatedAt: String = "", + val closedAt: String? = null, +) { + val displayName: String get() = title.ifBlank { fileName }.ifBlank { "未命名稿件" } +} + +@Serializable +data class SubmissionListDto( + val items: List = emptyList(), + val total: Int = 0, +) + +/** 投稿统计 GET /api/v1/submissions/stats */ +@Serializable +data class SubmissionStatsDto( + val pending: Int = 0, + val reviewing: Int = 0, + val approved: Int = 0, + val rejected: Int = 0, + val needRevision: Int = 0, + val missingAudio: Int = 0, + val closed: Int = 0, + val draft: Int = 0, + val revised: Int = 0, +) { + /** 进行中:待审 + 审核中 + 需修改 + 缺音频 */ + val processing: Int get() = pending + reviewing + needRevision + missingAudio + val total: Int get() = pending + reviewing + approved + rejected + draft + closed + revised +} + +/** 词库搜索 GET /api/v1/search */ +@Serializable +data class SearchHitDto( + val id: String = "", + val musicNames: List = emptyList(), + val artists: List = emptyList(), + val albums: List = emptyList(), + val platformIds: Map> = emptyMap(), + val rawLyricFile: String = "", + val wordCount: Int = 0, + val lineCount: Int = 0, + val lyricSnippet: String = "", +) + +@Serializable +data class SearchResultDto( + val hits: List = emptyList(), + val totalHits: Int = 0, +) + +/** 网易云歌手 / 专辑,搜索结果里的嵌套结构 */ +@Serializable +data class NcmArtistDto(val name: String = "") + +@Serializable +data class NcmAlbumDto( + val name: String = "", + val picUrl: String = "", +) + +/** + * 网易云单曲 GET /api/v1/ncm/search + * + * 后端是 { code, message, data: { code, data: [song] } } 双层结构, + * 字段名沿用上游(id 是数字、artists 是对象数组),不要按中文歌名猜。 + */ +@Serializable +data class NcmSongDto( + val id: Long = 0, + val name: String = "", + val songName: String = "", + val picUrl: String = "", + val artists: List = emptyList(), + val album: NcmAlbumDto? = null, + val fee: Int = 0, + @Serializable(with = FlexibleDuration::class) val duration: Long = 0, + @Serializable(with = FlexibleDuration::class) val dt: Long = 0, +) { + val displayName: String get() = name.ifBlank { songName }.ifBlank { "未知歌曲" } + val displayArtist: String + get() = artists.joinToString(" / ") { it.name }.ifBlank { "未知歌手" } + val displayCover: String + get() = httpsCover(picUrl.ifBlank { album?.picUrl.orEmpty() }) + val displayId: String get() = if (id > 0) id.toString() else "" + val displayDuration: Long get() = if (dt > 0) dt else duration + val displayAlbum: String get() = album?.name.orEmpty() +} + +/** + * 网易云上游的数字字段不稳定:同一个字段有时是数字、有时是字符串 + * (实测 parse-music 的 size 是 "7.23MB",search 的 alias 可能是 "")。 + * 这个序列化器两种都接:字符串取前导数字,非数字给 0。 + */ +object FlexibleLong : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("FlexibleLong", PrimitiveKind.LONG) + + override fun deserialize(decoder: Decoder): Long { + val primitive = (decoder as? JsonDecoder)?.decodeJsonElement() as? JsonPrimitive + ?: return decoder.decodeLong() + val raw = primitive.content.trim() + if (primitive.isString) { + val leading = Regex("^[0-9]+").find(raw) ?: return 0L + return leading.value.toLongOrNull() ?: 0L + } + return raw.toLongOrNull() ?: raw.toDoubleOrNull()?.toLong() ?: 0L + } + + override fun serialize(encoder: Encoder, value: Long) { + encoder.encodeLong(value) + } +} + +/** + * 时长字段有两种形态:搜索接口返回 "04:12" 这种字符串, + * 其它接口返回毫秒数字。统一归一成毫秒,避免 FlexibleLong + * 把 "04:12" 取成 4 毫秒。 + */ +object FlexibleDuration : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("FlexibleDuration", PrimitiveKind.LONG) + + private val COLON = Regex("^(\\d{1,3}):(\\d{1,2})(?::(\\d{1,2}))?$") + + override fun deserialize(decoder: Decoder): Long { + val primitive = (decoder as? JsonDecoder)?.decodeJsonElement() as? JsonPrimitive + ?: return decoder.decodeLong() + val raw = primitive.content.trim() + COLON.matchEntire(raw)?.let { match -> + val first = match.groupValues[1].toLongOrNull() ?: return 0L + val second = match.groupValues[2].toLongOrNull() ?: return 0L + val third = match.groupValues[3].takeIf { it.isNotEmpty() }?.toLongOrNull() + return if (third == null) { + first * 60_000 + second * 1_000 + } else { + first * 3_600_000 + second * 60_000 + third * 1_000 + } + } + return raw.toLongOrNull() ?: raw.toDoubleOrNull()?.toLong() ?: 0L + } + + override fun serialize(encoder: Encoder, value: Long) { + encoder.encodeLong(value) + } +} + +@Serializable +data class NcmSearchDto( + val code: Int = 0, + val data: List = emptyList(), +) { + val songs: List get() = data +} + +/** 歌单曲目(字段名与上游 models.Track 一致:ar / al / dt) */ +@Serializable +data class NcmTrackDto( + val id: Long = 0, + val name: String = "", + val ar: List = emptyList(), + val al: NcmAlbumDto? = null, + @Serializable(with = FlexibleLong::class) val dt: Long = 0, +) { + val displayArtist: String get() = ar.joinToString(" / ") { it.name }.ifBlank { "未知歌手" } + val displayCover: String get() = httpsCover(al?.picUrl.orEmpty()) + val displayId: String get() = if (id > 0) id.toString() else "" +} + +@Serializable +data class NcmPlaylistDto( + val id: Long = 0, + val name: String = "", + val coverImgUrl: String = "", + val trackCount: Int = 0, + val creator: NcmCreatorDto? = null, + val tracks: List = emptyList(), +) + +@Serializable +data class NcmCreatorDto(val nickname: String = "") + +/** parse-playlist 的外层:{ code, playlist } */ +@Serializable +data class NcmPlaylistWrapperDto(val playlist: NcmPlaylistDto? = null) + +/** + * parse-music 结果 GET /api/v1/ncm/parse-music + * + * 上游字段名不统一(url / name / ar_name / pic 各有别名), + * 这里把所有别名都接住,取值走 displayXxx。 + */ +@Serializable +data class NcmMusicDto( + val url: String = "", + val name: String = "", + val songName: String = "", + @SerialName("ar_name") val arName: String = "", + val artistName: String = "", + val pic: String = "", + val cover: String = "", + val coverUrl: String = "", + val albumPic: String = "", + val albumName: String = "", + @Serializable(with = FlexibleLong::class) val duration: Long = 0, + @Serializable(with = FlexibleLong::class) val dt: Long = 0, + val level: String = "", + /** 上游实测返回的是 "7.23MB" 这种字符串,不能声明成 Long */ + val size: String = "", + val lyric: String = "", + val tlyric: String = "", +) { + val displayUrl: String get() = url + val displayName: String get() = name.ifBlank { songName } + val displayArtist: String get() = arName.ifBlank { artistName } + val displayCover: String + get() = httpsCover(cover.ifBlank { coverUrl }.ifBlank { pic }.ifBlank { albumPic }) + val displayDuration: Long get() = if (dt > 0) dt else duration +} + +/** + * 网易云图片地址统一升级到 HTTPS。 + * 上游搜索接口常返回 http://p1.music.126.net/…,生产包禁止明文请求, + * 不升级会导致封面 URL 存在但图片加载失败(列表只剩灰块)。 + */ +internal fun httpsCover(url: String): String = + if (url.startsWith("http://")) "https://" + url.removePrefix("http://") else url + +@Serializable +data class LikeStatusDto(val liked: Boolean = false, val likeCount: Int = 0) + +/* ===== 投稿相关 ===== */ + +/** POST /api/v1/submissions/validate 返回(字段名与后端 TtmlValidateResult 一致) */ +@Serializable +data class TtmlValidationDto( + val valid: Boolean = false, + val errors: List = emptyList(), + val parseError: String = "", + val regeneratedTtml: String = "", + val metadata: JsonObject = JsonObject(emptyMap()), +) { + /** TTML 元数据里的标题列表,用于创建投稿时的 title */ + val titles: List get() = metadata.stringValues("title") + val language: String get() = metadata["language"]?.jsonString().orEmpty() +} + +/** POST /api/v1/submissions 入参,metadata 直接沿用校验结果的 snake_case 结构 */ +@Serializable +data class CreateSubmissionRequest( + val title: String, + val metadata: JsonObject, + val fileName: String, + val notes: String? = null, + val tags: List? = null, + val language: String? = null, + val languages: List? = null, + val status: String = "pending", +) + +/** 通用 { id } / { tempKey } / { fileName } / { available } 返回 */ +@Serializable +data class IdDto(val id: Long = 0) + +@Serializable +data class TempKeyDto(val tempKey: String = "") + +@Serializable +data class UploadedFileDto(val fileName: String = "") + +@Serializable +data class DateAvailabilityDto(val available: Boolean = true) + +@Serializable +data class UrlRequest(val url: String) + +/** POST /api/v1/daily-recommendations 入参(后端用 PostForm 读取) */ +data class CreateDailyRecRequest( + val songName: String, + val artist: String, + val coverTempKey: String, + val date: String, + val comment: String, + val ncmId: String? = null, +) + +private fun JsonObject.stringValues(key: String): List = + (this[key] as? JsonArray) + ?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } + ?.filter { it.isNotBlank() } + .orEmpty() + +private fun kotlinx.serialization.json.JsonElement.jsonString(): String? = + (this as? JsonPrimitive)?.contentOrNull + +@Serializable +data class UpdatedDto(val updated: Int = 0) + +@Serializable +data class ProfileUpdateRequest( + val displayName: String? = null, + val email: String? = null, + val avatar: String? = null, +) + +/** 每日推荐投稿列表(创作中心) */ +@Serializable +data class DailySubmissionListDto( + val items: List = emptyList(), + val total: Int = 0, +) + +/** 搜索 IP 投稿条目 */ +@Serializable +data class SearchIpSubmissionDto( + val id: Long = 0, + val title: String = "", + val status: String = "", + val createdAt: String = "", +) + +@Serializable +data class SearchIpListDto( + val items: List = emptyList(), + val total: Int = 0, +) + +/** 单日活动 GET /api/v1/users/me/activity */ +@Serializable +data class ActivityDayDto( + val date: String = "", + val count: Int = 0, + val byType: Map = emptyMap(), +) + +/** 年度活动聚合 GET /api/v1/users/me/activity?year= */ +@Serializable +data class UserActivityDto( + val year: Int = 0, + val years: List = emptyList(), + val days: List = emptyList(), + val total: Int = 0, + val byType: Map = emptyMap(), +) diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/Dto.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/Dto.kt index bb6197d..f2747fa 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/Dto.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/Dto.kt @@ -36,6 +36,29 @@ data class LoginRequest(val username: String, val password: String) @Serializable data class LoginByCodeRequest(val dest: String, val code: String) +/** 账号存在性预检 POST /api/v1/auth/check-user */ +@Serializable +data class CheckUserRequest( + val checkType: String, + val dest: String, + val method: String = "login", +) + +/** + * Casdoor 人机验证配置 GET /api/v1/auth/captcha + * clientId2 = 阿里云 SceneId,clientSecret2 = 阿里云 prefix + */ +@Serializable +data class CaptchaConfig( + val type: String = "", + @SerialName("clientId2") val sceneId: String = "", + @SerialName("clientSecret2") val prefix: String = "", +) { + /** 与网页端一致:none / default / 空串都视为无需人机验证 */ + val required: Boolean + get() = type.isNotBlank() && type != "none" && type != "default" +} + @Serializable data class SendCodeRequest( val checkType: String, diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/NetworkModule.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/NetworkModule.kt index 25f0e2c..ccdd76b 100644 --- a/android/app/src/main/java/dev/amll/hub/android/data/remote/NetworkModule.kt +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/NetworkModule.kt @@ -1,7 +1,10 @@ package dev.amll.hub.android.data.remote +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import dev.amll.hub.android.BuildConfig -import dev.amll.hub.android.data.local.AuthStore import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -9,44 +12,59 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory import java.util.concurrent.TimeUnit -import javax.inject.Inject import javax.inject.Singleton -/** Retrofit / OkHttp 单例装配 */ -@Singleton -class NetworkModule @Inject constructor( - private val authStore: AuthStore, -) { - val json: Json = Json { +/** Retrofit / OkHttp / Api 的 Hilt 装配,生命周期完全由 Hilt 管理 */ +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideJson(): Json = Json { ignoreUnknownKeys = true explicitNulls = false coerceInputValues = true } - private val client: OkHttpClient by lazy { + @Provides + @Singleton + fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient = OkHttpClient.Builder() - .addInterceptor(AuthInterceptor(authStore)) + .addInterceptor(authInterceptor) .apply { if (BuildConfig.DEBUG) { addInterceptor( HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC - } + }, ) } } .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + // 投稿要上传 TTML / 音源 / 封面,30s 在移动网络下容易半路超时 + .writeTimeout(120, TimeUnit.SECONDS) .build() - } - val authApi: AuthApi by lazy { + @Provides + @Singleton + fun provideRetrofit(client: OkHttpClient, json: Json): Retrofit = Retrofit.Builder() .baseUrl(BuildConfig.API_BASE_URL.trimEnd('/') + "/") .client(client) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() - .create(AuthApi::class.java) - } + + @Provides + @Singleton + fun provideAuthApi(retrofit: Retrofit): AuthApi = retrofit.create(AuthApi::class.java) + + @Provides + @Singleton + fun provideContentApi(retrofit: Retrofit): ContentApi = retrofit.create(ContentApi::class.java) + + @Provides + @Singleton + fun provideRawApi(retrofit: Retrofit): RawApi = retrofit.create(RawApi::class.java) } diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/RawApi.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/RawApi.kt new file mode 100644 index 0000000..ab86a53 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/RawApi.kt @@ -0,0 +1,27 @@ +package dev.amll.hub.android.data.remote + +import okhttp3.ResponseBody +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * 少数接口不套 {code,message,data} 包装: + * - ncm 解析直接返回 JSON + * - TTML 歌词直接返回字节流 + */ +interface RawApi { + + @GET("api/v1/ncm/parse-music") + suspend fun parseMusicRaw( + @Query("songId") songId: String, + @Query("level") level: String = "exhigh", + ): ResponseBody + + @GET("api/v1/lyrics/ncm-lyrics/{songId}") + suspend fun ncmLyric(@Path("songId") songId: String): ResponseBody + + /** 词库自己的 TTML(搜索命中的 rawLyricFile),优先级比 ncm-lyrics 高 */ + @GET("api/v1/lyrics/raw-lyrics/{filename}") + suspend fun rawLyric(@Path("filename") filename: String): ResponseBody +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/remote/Urls.kt b/android/app/src/main/java/dev/amll/hub/android/data/remote/Urls.kt new file mode 100644 index 0000000..2c893ae --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/remote/Urls.kt @@ -0,0 +1,40 @@ +package dev.amll.hub.android.data.remote + +import dev.amll.hub.android.BuildConfig +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +/** + * 拼接后端资源地址。统一走 OkHttp HttpUrl,路径段与查询参数由它负责转义, + * 避免手写 URLEncoder 时漏掉 `/`、`&`、`#` 等字符。 + */ +object Urls { + + private val baseUrl = BuildConfig.API_BASE_URL.trimEnd('/').toHttpUrlOrNull() + + /** 每日推荐封面:coverKey 是 MinIO 对象 key,传 width 走缩略图 */ + fun dailyCover(key: String?, width: Int? = null): String? { + val raw = key?.trim().orEmpty() + if (raw.isEmpty()) return null + val base = baseUrl ?: return null + val builder = base.newBuilder() + .addPathSegments("api/v1/daily-recommendations/image") + .addPathSegment(raw) + if (width != null) builder.addQueryParameter("w", width.toString()) + return builder.build().toString() + } + + /** 用户头像等字段可能是相对路径(/uploads/...),统一补成绝对地址 */ + fun absolute(path: String?): String? { + val raw = path?.trim().orEmpty() + if (raw.isEmpty()) return null + return when { + raw.startsWith("http://") || raw.startsWith("https://") -> raw + raw.startsWith("//") -> "https:$raw" + baseUrl == null -> null + else -> baseUrl.newBuilder() + .addPathSegments(raw.removePrefix("/")) + .build() + .toString() + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/repo/ContentRepository.kt b/android/app/src/main/java/dev/amll/hub/android/data/repo/ContentRepository.kt new file mode 100644 index 0000000..f6c7956 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/repo/ContentRepository.kt @@ -0,0 +1,243 @@ +package dev.amll.hub.android.data.repo + +import dev.amll.hub.android.data.remote.ApiErrorParser +import dev.amll.hub.android.data.remote.ApiException +import dev.amll.hub.android.data.remote.ContentApi +import dev.amll.hub.android.data.remote.CreateDailyRecRequest +import dev.amll.hub.android.data.remote.CreateSubmissionRequest +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.LatestSongDto +import dev.amll.hub.android.data.remote.LikeStatusDto +import dev.amll.hub.android.data.remote.MessageDto +import dev.amll.hub.android.data.remote.NcmMusicDto +import dev.amll.hub.android.data.remote.NcmPlaylistDto +import dev.amll.hub.android.data.remote.NcmSongDto +import dev.amll.hub.android.data.remote.ProfileUpdateRequest +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.data.remote.RawApi +import dev.amll.hub.android.data.remote.SearchHitDto +import dev.amll.hub.android.data.remote.SearchIpSubmissionDto +import dev.amll.hub.android.data.remote.StatsDto +import dev.amll.hub.android.data.remote.SubmissionDto +import dev.amll.hub.android.data.remote.SubmissionStatsDto +import dev.amll.hub.android.data.remote.TtmlValidationDto +import dev.amll.hub.android.data.remote.UrlRequest +import dev.amll.hub.android.data.remote.UserActivityDto +import dev.amll.hub.android.data.remote.UserProfile +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 内容仓库:所有接口都以 [Result] 返回,ViewModel 据此区分 + * 「成功但为空」与「请求失败」,不会再把错误折叠成空列表。 + */ +@Singleton +class ContentRepository @Inject constructor( + private val api: ContentApi, + private val raw: RawApi, +) { + + suspend fun stats(): Result = result { api.stats().data ?: fail("服务端返回空数据") } + + suspend fun latest(): Result> = + result { api.latestSongs().data.orEmpty() } + + suspend fun daily(): Result> = + result { api.dailyRecommendations().data.orEmpty() } + + suspend fun today(): Result = result { api.todayRecommendation().data } + + suspend fun ranking(days: Int): Result> = + result { api.notFoundRanking(days = days).data?.items.orEmpty() } + + suspend fun notifications(type: String? = null): Result> = + result { api.notifications(type = type).data?.items.orEmpty() } + + suspend fun submissions(status: String = "all"): Result> = + result { api.submissions(status = status).data?.items.orEmpty() } + + suspend fun submissionStats(): Result = + result { api.submissionStats().data ?: fail("服务端返回空数据") } + + /** 每日推荐投稿(创作中心) */ + suspend fun dailySubmissions(): Result> = + result { api.myDailySubmissions().data?.items.orEmpty() } + + /** 搜索 IP 投稿(创作中心) */ + suspend fun searchIpSubmissions(): Result> = + result { api.mySearchIpSubmissions().data?.items.orEmpty() } + + /** 关闭自己的稿件 */ + suspend fun closeSubmission(id: Long): Result = + result { api.closeSubmission(id).isSuccessful } + + /** 确认(采用)审核员修订版 */ + suspend fun adoptRevision(id: Long): Result = + result { api.adoptRevision(id).isSuccessful } + + /** 退回修订版,要求重新修改 */ + suspend fun rejectRevision(id: Long): Result = + result { api.rejectRevision(id).isSuccessful } + + /** TTML 原文,用于歌词页 */ + suspend fun lyricTtml(songId: String): Result = + result { raw.ncmLyric(songId).string() } + + /** 词库里的 TTML(搜索命中的 rawLyricFile) */ + suspend fun rawLyricTtml(fileName: String): Result = + result { raw.rawLyric(fileName).string() } + + /** + * 解析单曲:音频直链 + 封面 / 歌手 / 标题 / 歌词。 + * 与网页端 playNcmSong 走的是同一个接口,所以「没封面」的歌也能补齐。 + */ + suspend fun parseMusic(songId: String, level: String = "exhigh"): Result = + result("解析失败") { + api.ncmParseMusic(songId, level).data ?: fail("解析失败(响应为空)") + } + + /** 音频直链(兼容老调用) */ + suspend fun audioUrl(songId: String): String? = + parseMusic(songId).getOrNull()?.displayUrl?.takeIf { it.isNotBlank() } + + /** 网易云关键词搜索,失败原因原样交给 UI 展示 */ + suspend fun ncmSearch(keyword: String, limit: Int = 20): Result> = + result("搜索失败") { api.ncmSearch(keyword, limit).data?.songs.orEmpty() } + + /** 网易云歌单解析 */ + suspend fun ncmPlaylist(playlistId: String): Result = + result("歌单解析失败") { + api.ncmParsePlaylist(playlistId).data?.playlist ?: fail("歌单解析失败(响应为空)") + } + + suspend fun updateDisplayName(name: String): Result = + result { api.updateProfile(ProfileUpdateRequest(displayName = name)).data ?: fail("服务端返回空数据") } + + suspend fun search(keyword: String, field: String = "all"): Result> = + result { api.search(keyword, field = field).data?.hits.orEmpty() } + + suspend fun submission(id: Long): Result = + result { api.submission(id).data ?: fail("服务端返回空数据") } + + /** 年度活动统计;未登录或接口异常返回失败,UI 走空态 */ + suspend fun activity(year: Int? = null): Result = + result { api.activity(year).data ?: fail("服务端返回空数据") } + + suspend fun markAllRead(): Result = result { api.markAllNotificationsRead() } + + suspend fun markRead(id: Long): Result = result { api.markNotificationRead(id) } + + suspend fun toggleLike(id: Long): Result = + result { api.toggleDailyLike(id).data ?: fail("服务端返回空数据") } + + suspend fun likeStatus(id: Long): Result = + result { api.dailyLikeStatus(id).data ?: fail("服务端返回空数据") } + + /* ===== 投稿:歌词投稿 + 每日推荐投稿 ===== */ + + /** 校验 TTML:与网页端一样把原文以 text/plain 传给后端代理 */ + suspend fun validateTtml(content: String): Result = + result("校验失败") { + api.validateTtml(content.toRequestBody("text/plain; charset=utf-8".toMediaType())).data + ?: fail("校验失败(响应为空)") + } + + /** 上传 TTML 到待审核区,返回后端保存的文件名 */ + suspend fun uploadTtml(fileName: String, bytes: ByteArray): Result = + result("上传失败") { + val filePart = MultipartBody.Part.createFormData( + "file", + fileName, + bytes.toRequestBody("application/xml".toMediaType()), + ) + val namePart = fileName.toRequestBody("text/plain; charset=utf-8".toMediaType()) + api.uploadTtml(filePart, namePart).data?.fileName?.takeIf { it.isNotBlank() } ?: fileName + } + + suspend fun createSubmission(request: CreateSubmissionRequest): Result = + result("投稿失败") { + api.createSubmission(request).data?.id?.takeIf { it > 0 } ?: fail("投稿失败(响应为空)") + } + + suspend fun uploadAudio( + submissionId: Long, + fileName: String, + bytes: ByteArray, + mime: String, + ): Result = result("音源上传失败") { + val part = MultipartBody.Part.createFormData( + "audio", + fileName, + bytes.toRequestBody(mime.toMediaType()), + ) + val idPart = submissionId.toString().toRequestBody("text/plain; charset=utf-8".toMediaType()) + val response = api.uploadAudio(part, idPart) + if (!response.isSuccessful) { + throw ApiException(response.code(), errorMessage(response.errorBody()?.string(), "音源上传失败")) + } + } + + suspend fun uploadDailyCover(fileName: String, bytes: ByteArray, mime: String): Result = + result("封面上传失败") { + val part = MultipartBody.Part.createFormData( + "file", + fileName, + bytes.toRequestBody(mime.toMediaType()), + ) + api.uploadDailyCover(part).data?.tempKey?.takeIf { it.isNotBlank() } + ?: fail("封面上传失败(响应为空)") + } + + suspend fun uploadDailyCoverFromUrl(url: String): Result = + result("封面自动载入失败") { + api.uploadDailyCoverFromUrl(UrlRequest(url)).data?.tempKey?.takeIf { it.isNotBlank() } + ?: fail("封面自动载入失败(响应为空)") + } + + suspend fun checkDailyDate(date: String): Result = + result("日期检查失败") { api.checkDailyDate(date).data?.available ?: true } + + suspend fun createDailyRecommendation(request: CreateDailyRecRequest): Result = + result("投稿失败") { + val plain = "text/plain; charset=utf-8".toMediaType() + api.createDailyRecommendation( + songName = request.songName.toRequestBody(plain), + artist = request.artist.toRequestBody(plain), + coverTempKey = request.coverTempKey.toRequestBody(plain), + date = request.date.toRequestBody(plain), + comment = request.comment.toRequestBody(plain), + ncmId = request.ncmId.orEmpty().toRequestBody(plain), + ).data?.id?.takeIf { it > 0 } ?: fail("投稿失败(响应为空)") + } + + /** Response 形式的接口不抛 HttpException,这里手动取后端 message */ + private fun errorMessage(raw: String?, fallback: String): String = runCatching { + val parsed = Json { ignoreUnknownKeys = true }.parseToJsonElement(raw.orEmpty()) as? JsonObject + (parsed?.get("message")?.jsonPrimitive?.contentOrNull)?.takeIf { it.isNotBlank() } + }.getOrNull() ?: fallback + + private fun fail(message: String): Nothing = throw ApiException(200, message) + + private suspend fun result( + fallbackPrefix: String = "请求失败", + block: suspend () -> T, + ): Result = withContext(Dispatchers.IO) { + try { + Result.success(block()) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(ApiErrorParser.map(e, fallbackPrefix)) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/repo/LyricRepository.kt b/android/app/src/main/java/dev/amll/hub/android/data/repo/LyricRepository.kt new file mode 100644 index 0000000..02af052 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/repo/LyricRepository.kt @@ -0,0 +1,58 @@ +package dev.amll.hub.android.data.repo + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 一首歌拿到的原始歌词文本。 + * + * 解析不在原生侧做:页面里的 AMLL 官方解析器(@applemusic-like-lyrics/lyric) + * 与网页端 PlayerBoot 用的是同一个库,逐字空格等信息只有它处理得对。 + */ +data class LyricSource( + /** 原始歌词文本(TTML 或 LRC);空串表示没有歌词 */ + val text: String = "", + /** "ttml" / "lrc";空串表示没有歌词 */ + val format: String = "", + /** LRC 的翻译文本(tlyric);TTML 自带翻译,这里为空 */ + val translation: String = "", + /** 原始 TTML,交给播放器缓存;LRC 兜底时为空 */ + val ttml: String = "", +) + +/** + * 歌词获取与三级回退(与网页端 PlayerBoot 一致): + * 1. 词库 TTML(搜索命中的 rawLyricFile) + * 2. ncm-lyrics 目录下的 TTML + * 3. parse-music 返回的 LRC(+ tlyric 合并翻译) + * + * 这里只负责取原文,格式判断与解析全部交给歌词页。 + */ +@Singleton +class LyricRepository @Inject constructor( + private val repo: ContentRepository, +) { + + suspend fun load( + songId: String, + rawLyricFile: String?, + ncmLyric: String, + ncmTLyric: String, + ): LyricSource = withContext(Dispatchers.Default) { + val candidates = listOfNotNull( + rawLyricFile?.takeIf { it.isNotBlank() }?.let { repo.rawLyricTtml(it).getOrNull() }, + repo.lyricTtml(songId).getOrNull(), + ) + val ttml = candidates.firstOrNull { !it.isNullOrBlank() } + if (!ttml.isNullOrBlank()) { + return@withContext LyricSource(text = ttml, format = "ttml", ttml = ttml) + } + + if (ncmLyric.isNotBlank()) { + return@withContext LyricSource(text = ncmLyric, format = "lrc", translation = ncmTLyric) + } + LyricSource() + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/data/repo/PlaybackRepository.kt b/android/app/src/main/java/dev/amll/hub/android/data/repo/PlaybackRepository.kt new file mode 100644 index 0000000..1e9d71a --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/data/repo/PlaybackRepository.kt @@ -0,0 +1,44 @@ +package dev.amll.hub.android.data.repo + +import dev.amll.hub.android.data.remote.NcmMusicDto +import dev.amll.hub.android.playback.NcmQuality +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 音源解析:按用户选择的音质请求,拿不到链接且请求不是 standard 时 + * 自动退到 standard 再试一次。实际拿到的音质交给 UI 判断是否已降级。 + */ +@Singleton +class PlaybackRepository @Inject constructor( + private val repo: ContentRepository, +) { + + data class Resolved( + val music: NcmMusicDto?, + val error: String?, + val requested: NcmQuality, + val actual: NcmQuality?, + ) + + suspend fun resolve(songId: String, level: String = "exhigh"): Resolved { + val requested = NcmQuality.fromKey(level) + ?: NcmQuality.normalizeLevel(level) + ?: NcmQuality.EXHIGH + var parseError: String? = null + var music = repo.parseMusic(songId, level) + .onFailure { parseError = it.message } + .getOrNull() + if (music?.displayUrl.isNullOrBlank() && requested != NcmQuality.STANDARD) { + music = repo.parseMusic(songId, level = NcmQuality.STANDARD.key) + .onFailure { parseError = it.message } + .getOrNull() + } + return Resolved( + music = music, + error = parseError, + requested = requested, + actual = NcmQuality.normalizeLevel(music?.level), + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/di/AppModule.kt b/android/app/src/main/java/dev/amll/hub/android/di/AppModule.kt deleted file mode 100644 index ebdc6e9..0000000 --- a/android/app/src/main/java/dev/amll/hub/android/di/AppModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.amll.hub.android.di - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dev.amll.hub.android.data.local.AuthStore -import dev.amll.hub.android.data.remote.AuthApi -import dev.amll.hub.android.data.remote.NetworkModule -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides - @Singleton - fun provideAuthStore(@ApplicationContext context: Context): AuthStore = AuthStore(context) - - @Provides - @Singleton - fun provideNetworkModule(authStore: AuthStore): NetworkModule = NetworkModule(authStore) - - @Provides - @Singleton - fun provideAuthApi(network: NetworkModule): AuthApi = network.authApi -} diff --git a/android/app/src/main/java/dev/amll/hub/android/download/DownloadPermissions.kt b/android/app/src/main/java/dev/amll/hub/android/download/DownloadPermissions.kt new file mode 100644 index 0000000..99c33cf --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/download/DownloadPermissions.kt @@ -0,0 +1,25 @@ +package dev.amll.hub.android.download + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat + +/** API 28 及以下写公共下载目录需要 WRITE_EXTERNAL_STORAGE;29+ 走 MediaStore。 */ +fun needsLegacyWritePermission(): Boolean = + Build.VERSION.SDK_INT <= Build.VERSION_CODES.P + +/** + * API 33+ 的通知权限。拒绝只影响通知展示,不阻塞下载; + * 低版本没有这个运行时权限,恒为 true。 + */ +fun hasNotificationPermission(context: Context): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + +fun hasLegacyWritePermission(context: Context): Boolean = + !needsLegacyWritePermission() || + ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == + PackageManager.PERMISSION_GRANTED diff --git a/android/app/src/main/java/dev/amll/hub/android/download/DownloadRequest.kt b/android/app/src/main/java/dev/amll/hub/android/download/DownloadRequest.kt new file mode 100644 index 0000000..cab039f --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/download/DownloadRequest.kt @@ -0,0 +1,14 @@ +package dev.amll.hub.android.download + +import kotlinx.serialization.Serializable + +/** 一次下载需要的曲目元信息,作为 JSON 随 Intent 传给 [DownloadService]。 */ +@Serializable +data class DownloadRequest( + val songId: String, + val name: String, + val artists: String, + val coverUrl: String? = null, + val albumName: String = "", + val level: String = "exhigh", +) diff --git a/android/app/src/main/java/dev/amll/hub/android/download/DownloadService.kt b/android/app/src/main/java/dev/amll/hub/android/download/DownloadService.kt new file mode 100644 index 0000000..3a93bf7 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/download/DownloadService.kt @@ -0,0 +1,610 @@ +package dev.amll.hub.android.download + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Environment +import android.os.IBinder +import android.provider.MediaStore +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.mpatric.mp3agic.ID3v24Tag +import com.mpatric.mp3agic.Mp3File +import dagger.hilt.android.AndroidEntryPoint +import dev.amll.hub.android.R +import dev.amll.hub.android.data.remote.NcmMusicDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jaudiotagger.audio.AudioFileIO +import org.jaudiotagger.audio.flac.metadatablock.MetadataBlockDataPicture +import org.jaudiotagger.tag.FieldKey +import org.jaudiotagger.tag.reference.PictureTypes +import java.io.File +import java.io.IOException +import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.inject.Inject + +/** + * 前台下载服务:单曲直接写系统下载目录,歌单(批量)先下到缓存、 + * 写完标签后打包成 ZIP 再写入下载目录。 + * + * 下载失败只计入 failed,不中断其余曲目;通知权限被拒绝也不影响执行。 + */ +@AndroidEntryPoint +class DownloadService : Service() { + + @Inject + lateinit var repository: ContentRepository + + @Inject + lateinit var store: DownloadStore + + @Inject + lateinit var client: OkHttpClient + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var job: Job? = null + + override fun onCreate() { + super.onCreate() + createChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + // 每次 startForegroundService 都要尽快进入前台态 + startForeground(NOTIFICATION_ID, buildNotification("下载", "准备中…", 0, indeterminate = true)) + + // 已有任务在跑时不叠加第二个任务,避免两个进度条互相覆盖 + if (job?.isActive == true) return START_NOT_STICKY + + val requests = intent?.getStringExtra(EXTRA_REQUESTS) + ?.let { raw -> runCatching { json.decodeFromString>(raw) }.getOrNull() } + .orEmpty() + if (requests.isEmpty()) { + finishWith("没有可下载的曲目", success = false) + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + return START_NOT_STICKY + } + + val zip = intent?.action == ACTION_ZIP + val zipName = intent?.getStringExtra(EXTRA_ZIP_NAME).orEmpty().ifBlank { DEFAULT_ZIP_NAME } + job = scope.launch { download(requests, zip, zipName) } + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } + + private suspend fun download(requests: List, zip: Boolean, zipName: String) { + store.start(requests.size, zip) + notifyProgress(0, 0, requests.size, "") + val dir = File(cacheDir, CACHE_DIR).apply { + deleteRecursively() + mkdirs() + } + try { + if (zip) { + downloadBatch(requests, zipName, dir) + } else { + downloadSingle(requests.first(), dir) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(TAG, "download task failed", e) + finishWith("下载失败:${e.message ?: "未知错误"}", success = false) + } finally { + dir.deleteRecursively() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + } + + private suspend fun downloadSingle(request: DownloadRequest, dir: File) { + val result = downloadOneOrNull(request, dir) + if (result == null) { + store.progress(0, 1, request.name) + notifyProgress(0, 1, 1, request.name) + finishWith("下载失败:${request.name}", success = false) + return + } + store.progress(1, 0, request.name) + notifyProgress(1, 0, 1, request.name) + + val displayName = uniqueDownloadName(result.baseName, result.extension) + val saved = writeToDownloads(result.file, displayName, result.format.mime) + if (saved) { + val tagSuffix = if (result.tagged) "" else "(标签写入失败)" + finishWith("已保存到 下载/AMLL Hub/$displayName$tagSuffix", success = true) + } else { + finishWith("写入下载目录失败:$displayName(API 28 及以下请授予存储权限)", success = false) + } + } + + private suspend fun downloadBatch(requests: List, zipName: String, dir: File) { + val total = requests.size + val done = AtomicInteger() + val failed = AtomicInteger() + val semaphore = Semaphore(MAX_CONCURRENT) + val results = coroutineScope { + requests.map { request -> + async { + semaphore.withPermit { + val result = downloadOneOrNull(request, dir) + val finishedDone: Int + val finishedFailed: Int + if (result == null) { + finishedFailed = failed.incrementAndGet() + finishedDone = done.get() + } else { + finishedDone = done.incrementAndGet() + finishedFailed = failed.get() + } + store.progress(finishedDone, finishedFailed, request.name) + notifyProgress(finishedDone, finishedFailed, total, request.name) + result + } + } + }.awaitAll() + } + + val files = results.filterNotNull() + if (files.isEmpty()) { + finishWith("下载失败:没有可用的曲目", success = false) + return + } + + val zipFile = File(dir, "${UUID.randomUUID()}.zip") + val zipResult = runCatching { + ZipOutputStream(zipFile.outputStream().buffered()).use { out -> + val usedNames = HashSet() + for (item in files) { + out.putNextEntry(ZipEntry(uniqueNameInZip(item.baseName, item.extension, usedNames))) + item.file.inputStream().use { it.copyTo(out) } + out.closeEntry() + } + } + } + if (zipResult.isFailure) { + finishWith("打包失败:${zipResult.exceptionOrNull()?.message ?: "未知错误"}", success = false) + return + } + + val zipBase = sanitize(zipName).ifBlank { DEFAULT_ZIP_NAME } + val displayName = uniqueDownloadName(zipBase, "zip") + if (!writeToDownloads(zipFile, displayName, ZIP_MIME)) { + finishWith("写入下载目录失败:$displayName(API 28 及以下请授予存储权限)", success = false) + return + } + val tagWarning = if (files.any { !it.tagged }) ",部分标签写入失败" else "" + finishWith( + "已保存到 下载/AMLL Hub/$displayName(成功 ${files.size} 首,失败 ${failed.get()} 首$tagWarning)", + success = true, + ) + } + + /** + * 解析 → 下载到缓存 → 写完标签。任何一步失败都返回 null, + * 由调用方计入失败数;取消异常照常上抛。 + */ + private suspend fun downloadOneOrNull(request: DownloadRequest, dir: File): DownloadResult? = try { + downloadOne(request, dir) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.w(TAG, "download failed: songId=${request.songId}", e) + null + } + + private suspend fun downloadOne(request: DownloadRequest, dir: File): DownloadResult { + val music = resolveMusic(request) + val part = File(dir, "${UUID.randomUUID()}.part") + val contentType = fetchInto(music.displayUrl, part) + + val format = detectFormat(contentType, music.displayUrl) + val extension = extensionFor(format, music.displayUrl) + val audio = File(dir, "${part.nameWithoutExtension}.$extension") + if (!part.renameTo(audio)) { + part.copyTo(audio, overwrite = true) + part.delete() + } + + val title = music.displayName.ifBlank { request.name }.ifBlank { "未知歌曲" } + val artist = music.displayArtist.ifBlank { request.artists }.ifBlank { "未知歌手" } + val album = music.albumName.ifBlank { request.albumName } + val cover = fetchCover(music.displayCover.ifBlank { request.coverUrl.orEmpty() }) + val tagged = applyTags(audio, format, Metadata(title, artist, album), cover) + return DownloadResult( + file = tagged.file, + baseName = sanitize("$title - $artist"), + extension = extension, + format = format, + tagged = tagged.tagged, + ) + } + + /** 按请求音质解析;无 URL 且不是 standard 时用 standard 重试一次。 */ + private suspend fun resolveMusic(request: DownloadRequest): NcmMusicDto { + val first = repository.parseMusic(request.songId, request.level).getOrNull() + if (first != null && first.displayUrl.isNotBlank()) return first + if (request.level != QUALITY_STANDARD) { + val fallback = repository.parseMusic(request.songId, QUALITY_STANDARD).getOrNull() + if (fallback != null && fallback.displayUrl.isNotBlank()) return fallback + } + throw IOException("无可用下载链接") + } + + /** 下载音频,返回响应 Content-Type。调用方已处于 IO 线程。 */ + private fun fetchInto(url: String, dest: File): String? { + client.newCall(Request.Builder().url(url).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("HTTP ${response.code}") + response.body.byteStream().use { input -> + dest.outputStream().use { output -> input.copyTo(output) } + } + return response.header("Content-Type") + } + } + + /** 封面拿不到(非 2xx、超大、网络异常)就跳过,不影响音频与文本标签。 */ + private fun fetchCover(url: String): Cover? { + if (url.isBlank()) return null + return try { + client.newCall(Request.Builder().url(url).build()).execute().use { response -> + if (!response.isSuccessful) return null + val bytes = response.body.bytes() + if (bytes.isEmpty() || bytes.size > MAX_COVER_BYTES) return null + val mime = response.header("Content-Type") + ?.substringBefore(';') + ?.trim() + ?.takeIf { it.startsWith("image/") } + ?: "image/jpeg" + Cover(bytes, mime) + } + } catch (e: Exception) { + Log.w(TAG, "cover download failed: $url", e) + null + } + } + + private fun applyTags(file: File, format: AudioFormat, meta: Metadata, cover: Cover?): TaggedFile = + when (format) { + AudioFormat.MP3 -> tagMp3(file, meta, cover) + AudioFormat.FLAC -> tagFlac(file, meta, cover) + AudioFormat.OTHER -> TaggedFile(file, true) + } + + /** mp3agic 不能覆盖原文件,先另存再删原文件。 */ + private fun tagMp3(file: File, meta: Metadata, cover: Cover?): TaggedFile { + val tagged = File(file.parentFile, "${file.nameWithoutExtension}_tagged.mp3") + return runCatching { + val mp3 = Mp3File(file.absolutePath) + val id3 = ID3v24Tag() + id3.setTitle(meta.title) + id3.setArtist(meta.artist) + if (meta.album.isNotBlank()) id3.setAlbum(meta.album) + cover?.let { id3.setAlbumImage(it.bytes, it.mime) } + mp3.setId3v2Tag(id3) + mp3.save(tagged.absolutePath) + file.delete() + TaggedFile(tagged, true) + }.getOrElse { e -> + Log.w(TAG, "mp3 tag failed: ${file.name}", e) + tagged.delete() + TaggedFile(file, false) + } + } + + /** + * FLAC 走 jaudiotagger。封面直接用 MetadataBlockDataPicture: + * Artwork 的 setImageFromData() 依赖 javax.imageio,Android 上没有实现。 + */ + private fun tagFlac(file: File, meta: Metadata, cover: Cover?): TaggedFile = + runCatching { + val audio = AudioFileIO.read(file) + val tag = audio.tagOrCreateAndSetDefault + tag.setField(FieldKey.TITLE, meta.title) + tag.setField(FieldKey.ARTIST, meta.artist) + if (meta.album.isNotBlank()) tag.setField(FieldKey.ALBUM, meta.album) + cover?.let { + tag.setField( + MetadataBlockDataPicture( + it.bytes, + PictureTypes.DEFAULT_ID, + it.mime, + "", + 0, + 0, + 0, + 0, + ), + ) + } + AudioFileIO.write(audio) + TaggedFile(file, true) + }.getOrElse { e -> + Log.w(TAG, "flac tag failed: ${file.name}", e) + TaggedFile(file, false) + } + + /* ===== 格式识别 ===== */ + + private fun detectFormat(contentType: String?, url: String): AudioFormat { + val type = contentType?.lowercase().orEmpty() + val path = url.substringBefore('?').substringBefore('#').lowercase() + return when { + "mpeg" in type || "mp3" in type || path.endsWith(".mp3") -> AudioFormat.MP3 + "flac" in type || path.endsWith(".flac") -> AudioFormat.FLAC + else -> AudioFormat.OTHER + } + } + + private fun extensionFor(format: AudioFormat, url: String): String = when (format) { + AudioFormat.MP3 -> "mp3" + AudioFormat.FLAC -> "flac" + AudioFormat.OTHER -> { + val name = url.substringBefore('?').substringBefore('#').substringAfterLast('/') + val ext = name.substringAfterLast('.', "").lowercase() + ext.takeIf { it.length in 2..5 && it.all { c -> c.isLetterOrDigit() } } ?: "bin" + } + } + + /* ===== 写入系统下载目录 ===== */ + + private fun writeToDownloads(file: File, displayName: String, mime: String): Boolean = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + writeViaMediaStore(file, displayName, mime) + } else { + writeViaLegacyDir(file, displayName) + } + }.onFailure { + Log.e(TAG, "save to downloads failed: $displayName", it) + }.isSuccess + + @RequiresApi(Build.VERSION_CODES.Q) + private fun writeViaMediaStore(file: File, displayName: String, mime: String) { + val resolver = contentResolver + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + put(MediaStore.MediaColumns.MIME_TYPE, mime) + put(MediaStore.MediaColumns.RELATIVE_PATH, DOWNLOAD_RELATIVE_PATH) + put(MediaStore.MediaColumns.IS_PENDING, 1) + } + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: throw IOException("MediaStore 插入失败") + try { + resolver.openOutputStream(uri)?.use { output -> + file.inputStream().use { it.copyTo(output) } + } ?: throw IOException("无法打开下载目录输出流") + values.clear() + values.put(MediaStore.MediaColumns.IS_PENDING, 0) + if (resolver.update(uri, values, null, null) <= 0) { + throw IOException("MediaStore 更新失败") + } + } catch (e: Exception) { + runCatching { resolver.delete(uri, null, null) } + throw e + } + } + + private fun writeViaLegacyDir(file: File, displayName: String) { + if (!hasLegacyWritePermission(this)) throw IOException("缺少存储写权限") + val dir = legacyDownloadDir() + if (!dir.isDirectory && !dir.mkdirs()) throw IOException("无法创建下载目录") + file.copyTo(File(dir, displayName), overwrite = false) + } + + private fun legacyDownloadDir(): File = + File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), DOWNLOAD_FOLDER) + + private fun uniqueDownloadName(base: String, extension: String): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + uniqueMediaStoreName(base, extension) + } else { + uniqueLegacyName(base, extension) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun uniqueMediaStoreName(base: String, extension: String): String { + var index = 0 + while (true) { + val candidate = if (index == 0) "$base.$extension" else "$base ($index).$extension" + val exists = runCatching { + contentResolver.query( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.MediaColumns._ID), + "${MediaStore.MediaColumns.DISPLAY_NAME}=? AND " + + "${MediaStore.MediaColumns.RELATIVE_PATH} LIKE ?", + arrayOf(candidate, "$DOWNLOAD_RELATIVE_PATH%"), + null, + )?.use { it.moveToFirst() } == true + }.getOrDefault(false) + if (!exists) return candidate + index++ + } + } + + private fun uniqueLegacyName(base: String, extension: String): String { + val dir = legacyDownloadDir() + var index = 0 + while (true) { + val candidate = if (index == 0) "$base.$extension" else "$base ($index).$extension" + if (!File(dir, candidate).exists()) return candidate + index++ + } + } + + private fun uniqueNameInZip(base: String, extension: String, used: MutableSet): String { + var index = 0 + while (true) { + val candidate = if (index == 0) "$base.$extension" else "$base ($index).$extension" + if (used.add(candidate)) return candidate + index++ + } + } + + private fun sanitize(raw: String): String = + raw.replace(ILLEGAL_CHARS, "_") + .replace(CONTROL_CHARS, "_") + .trim() + .trimEnd('.') + .take(MAX_NAME_LENGTH) + .ifBlank { "未命名" } + + /* ===== 通知 ===== */ + + private fun createChannel() { + val manager = getSystemService(NotificationManager::class.java) ?: return + val channel = NotificationChannel(CHANNEL_ID, "下载", NotificationManager.IMPORTANCE_LOW).apply { + description = "网易云歌曲下载进度" + setShowBadge(false) + } + manager.createNotificationChannel(channel) + } + + private fun buildNotification( + title: String, + text: String, + progress: Int?, + indeterminate: Boolean = false, + ): Notification { + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_music) + .setContentTitle(title) + .setContentText(text) + .setOnlyAlertOnce(true) + .setOngoing(progress != null) + .setSilent(true) + if (progress != null) { + builder.setProgress(100, progress.coerceIn(0, 100), indeterminate) + } else { + builder.setProgress(0, 0, false) + } + return builder.build() + } + + private fun notifyProgress(done: Int, failed: Int, total: Int, currentName: String) { + val finished = done + failed + val pct = if (total <= 0) 0 else finished * 100 / total + val text = buildString { + if (currentName.isNotBlank()) append(currentName).append(" · ") + append("$finished/$total") + if (failed > 0) append(",失败 $failed") + } + updateNotification(buildNotification("下载中", text, pct)) + } + + private fun finishWith(message: String, success: Boolean) { + store.finish(message) + updateNotification( + buildNotification( + title = if (success) "下载完成" else "下载失败", + text = message, + progress = null, + ), + ) + } + + @SuppressLint("MissingPermission", "NotificationPermission") + private fun updateNotification(notification: Notification) { + // 没授予 POST_NOTIFICATIONS 时 notify 会被系统静默丢弃,不影响下载本身 + runCatching { NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification) } + .onFailure { Log.w(TAG, "update notification failed", it) } + } + + private data class DownloadResult( + val file: File, + val baseName: String, + val extension: String, + val format: AudioFormat, + val tagged: Boolean, + ) + + private data class TaggedFile(val file: File, val tagged: Boolean) + + private data class Metadata(val title: String, val artist: String, val album: String) + + private data class Cover(val bytes: ByteArray, val mime: String) + + private enum class AudioFormat(val mime: String) { + MP3("audio/mpeg"), + FLAC("audio/flac"), + OTHER("application/octet-stream"), + } + + companion object { + const val ACTION_SINGLE = "dev.amll.hub.android.download.action.SINGLE" + const val ACTION_ZIP = "dev.amll.hub.android.download.action.ZIP" + const val EXTRA_REQUESTS = "requests" + const val EXTRA_ZIP_NAME = "zipName" + + private const val TAG = "DownloadService" + private const val CHANNEL_ID = "amll_download" + private const val NOTIFICATION_ID = 1002 + private const val CACHE_DIR = "download_tmp" + private const val MAX_CONCURRENT = 3 + private const val MAX_COVER_BYTES = 5 * 1024 * 1024 + private const val MAX_NAME_LENGTH = 120 + private const val DEFAULT_ZIP_NAME = "歌单" + private const val QUALITY_STANDARD = "standard" + private const val DOWNLOAD_FOLDER = "AMLL Hub" + private const val DOWNLOAD_RELATIVE_PATH = "Download/AMLL Hub" + private const val ZIP_MIME = "application/zip" + + private val ILLEGAL_CHARS = Regex("""[<>:"/\\|?*]""") + private val CONTROL_CHARS = Regex("[\\x00-\\x1F]") + + private val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + coerceInputValues = true + } + + fun start( + context: Context, + requests: List, + zip: Boolean, + zipName: String = DEFAULT_ZIP_NAME, + ) { + if (requests.isEmpty()) return + val intent = Intent(context, DownloadService::class.java).apply { + action = if (zip) ACTION_ZIP else ACTION_SINGLE + putExtra(EXTRA_REQUESTS, json.encodeToString(requests)) + putExtra(EXTRA_ZIP_NAME, zipName) + } + ContextCompat.startForegroundService(context, intent) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/download/DownloadStore.kt b/android/app/src/main/java/dev/amll/hub/android/download/DownloadStore.kt new file mode 100644 index 0000000..98886e8 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/download/DownloadStore.kt @@ -0,0 +1,41 @@ +package dev.amll.hub.android.download + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 下载进度单例:前台服务写入,解析页读取。 + * 批量下载暂时只能有一个任务,新任务会覆盖上一次的完成状态。 + */ +@Singleton +class DownloadStore @Inject constructor() { + + data class State( + val running: Boolean = false, + val total: Int = 0, + val done: Int = 0, + val failed: Int = 0, + val currentName: String = "", + val zip: Boolean = false, + val message: String? = null, + ) + + private val _state = MutableStateFlow(State()) + val state: StateFlow = _state.asStateFlow() + + fun start(total: Int, zip: Boolean) = _state.update { + State(running = true, total = total, zip = zip) + } + + fun progress(done: Int, failed: Int, currentName: String) = _state.update { + it.copy(running = true, done = done, failed = failed, currentName = currentName, message = null) + } + + fun finish(message: String? = null) = _state.update { + it.copy(running = false, currentName = "", message = message) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/nav/AmllNavGraph.kt b/android/app/src/main/java/dev/amll/hub/android/nav/AmllNavGraph.kt index ab451bf..1b92655 100644 --- a/android/app/src/main/java/dev/amll/hub/android/nav/AmllNavGraph.kt +++ b/android/app/src/main/java/dev/amll/hub/android/nav/AmllNavGraph.kt @@ -1,64 +1,107 @@ package dev.amll.hub.android.nav +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavType +import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import dev.amll.hub.android.data.remote.UserProfile +import androidx.navigation.toRoute +import dev.amll.hub.android.R +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.AppShell +import dev.amll.hub.android.ui.screen.CreatorCenterScreen +import dev.amll.hub.android.ui.screen.CreatorViewModel +import dev.amll.hub.android.ui.screen.DailyScreen +import dev.amll.hub.android.ui.screen.DailySubmitScreen +import dev.amll.hub.android.ui.screen.DailyViewModel +import dev.amll.hub.android.ui.screen.DiscoverScreen import dev.amll.hub.android.ui.screen.HomeScreen +import dev.amll.hub.android.ui.screen.HomeViewModel +import dev.amll.hub.android.ui.screen.LatestSongsScreen import dev.amll.hub.android.ui.screen.LoginScreen +import dev.amll.hub.android.ui.screen.LyricScreen +import dev.amll.hub.android.ui.screen.LyricSubmitScreen +import dev.amll.hub.android.ui.screen.LyricViewModel +import dev.amll.hub.android.ui.screen.MessagesScreen +import dev.amll.hub.android.ui.screen.MessagesViewModel +import dev.amll.hub.android.ui.screen.NcmParseScreen +import dev.amll.hub.android.ui.screen.PlaybackViewModel +import dev.amll.hub.android.ui.screen.ProfileEditScreen +import dev.amll.hub.android.ui.screen.ProfileScreen +import dev.amll.hub.android.ui.screen.ProfileViewModel +import dev.amll.hub.android.ui.screen.RankingScreen +import dev.amll.hub.android.ui.screen.RankingViewModel +import dev.amll.hub.android.ui.screen.ReviewQueueScreen import dev.amll.hub.android.ui.screen.ScanScreen +import dev.amll.hub.android.ui.screen.SearchScreen +import dev.amll.hub.android.ui.screen.SearchViewModel +import dev.amll.hub.android.ui.screen.SubmissionDetailScreen import dev.amll.hub.android.ui.session.SessionState import dev.amll.hub.android.ui.session.SessionViewModel +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font -object Routes { - const val HOME = "home" +/** Tab 在底栏中的下标;非 Tab 页面返回 null,用于判断左右滑动方向 */ +private fun NavBackStackEntry.tabIndex(): Int? = + TabRouteNames.indexOf(destination.route.orEmpty()).takeIf { it >= 0 } - /** 扫码页,ticket 可为空(手动进入扫码器)或由 deep link 带入 */ - const val SCAN = "scan?ticket={ticket}" - fun scan(ticket: String? = null): String = - if (ticket.isNullOrBlank()) "scan" else "scan?ticket=$ticket" +/** 目标页在右(或无法判断)时向前;否则向后 */ +private fun AnimatedContentTransitionScope.forward(): Boolean { + val from = initialState.tabIndex() + val to = targetState.tabIndex() + return from == null || to == null || to >= from } +/** 是不是歌词页(它自己带纵向转场,周围页面不应该再横向滑动) */ +private fun NavBackStackEntry.isLyricRoute(): Boolean = + destination.route == LyricRouteName + /** - * 顶层导航,完全由 [SessionViewModel] 的持久化登录态驱动: - * - Loading:读本地 token 期间的启动占位,避免登录页一闪而过 - * - LoggedIn:首页 + 扫码授权页 - * - LoggedOut:仅登录页,未登录不提供「扫码登录网页端」入口 - * - * 登录 / 登出只更新登录态,分区切换由状态完成,无需手动推栈。 + * 顶层导航:登录态决定进 App 还是登录页,登录后是 5 Tab + 二级页。 + * 转场统一为横向滑动:推进从右滑入,返回从左侧退回;Tab 之间按目标下标决定方向。 */ @Composable fun AmllNavGraph( - /** deep link 带进来的 ticket,非空时登录后直接弹扫码确认页 */ pendingTicket: String? = null, onTicketConsumed: () -> Unit = {}, sessionViewModel: SessionViewModel = hiltViewModel(), ) { val session by sessionViewModel.state.collectAsStateWithLifecycle() - when (val state = session) { - SessionState.Loading -> SplashPane() + when (session) { + SessionState.Loading -> SplashScreen() is SessionState.LoggedIn -> LoggedInNav( - profile = state.profile, pendingTicket = pendingTicket, onTicketConsumed = onTicketConsumed, - onLoggedOut = { sessionViewModel.logout() }, - onRefreshProfile = { sessionViewModel.refreshProfile() }, ) SessionState.LoggedOut -> LoginScreen( @@ -67,66 +110,135 @@ fun AmllNavGraph( } } -/** 登录后的导航分区:首页 + 扫码授权页 */ @Composable private fun LoggedInNav( - profile: UserProfile?, pendingTicket: String?, onTicketConsumed: () -> Unit, - onLoggedOut: () -> Unit, - onRefreshProfile: () -> Unit, ) { val navController = rememberNavController() - // 已登录时 deep link 直接进扫码确认页;未登录时 ticket 会被保留,登录后再处理 + // 各 feature ViewModel 挂在 Activity 作用域:切 Tab 不重复请求, + // 首页/我的共用同一份 profile,创作中心三个页面共用同一份稿件列表 + val homeVm: HomeViewModel = hiltViewModel() + val rankingVm: RankingViewModel = hiltViewModel() + val dailyVm: DailyViewModel = hiltViewModel() + val messagesVm: MessagesViewModel = hiltViewModel() + val creatorVm: CreatorViewModel = hiltViewModel() + val searchVm: SearchViewModel = hiltViewModel() + val profileVm: ProfileViewModel = hiltViewModel() + val lyricVm: LyricViewModel = hiltViewModel() + val playbackVm: PlaybackViewModel = hiltViewModel() + + val nav = remember(navController, lyricVm) { + AppNav( + navController = navController, + onOpenTrack = { songId, title, artist, cover, rawLyricFile -> + lyricVm.openTrack(songId, title, artist, cover, rawLyricFile) + }, + onPlayerLyric = { lyricVm.ensurePlayerLyric() }, + ) + } + LaunchedEffect(pendingTicket) { val ticket = pendingTicket?.takeIf { it.isNotBlank() } ?: return@LaunchedEffect onTicketConsumed() - navController.navigate(Routes.scan(ticket)) + navController.navigate(ScanRoute(ticket)) } - NavHost(navController = navController, startDestination = Routes.HOME) { - composable(Routes.HOME) { - HomeScreen( - profile = profile, - onScanQr = { navController.navigate(Routes.scan()) }, - onLoggedOut = onLoggedOut, - onRefreshProfile = onRefreshProfile, - ) - } + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route + val selectedTab = TabRouteNames.indexOf(currentRoute.orEmpty()) - composable( - route = Routes.SCAN, - arguments = listOf( - navArgument("ticket") { - type = NavType.StringType - nullable = true - defaultValue = null + // 底栏挂在外壳上:切页时只有内容滑动,栏本身不动 + AppShell( + playerVm = playbackVm, + lyricVm = lyricVm, + nav = nav, + selectedTab = selectedTab, + tabsVisible = selectedTab >= 0, + ) { + NavHost( + navController = navController, + startDestination = HomeRoute, + // 歌词页只有纵向转场;它进出的那一侧保持静止,否则会多出一段左移动画 + enterTransition = { + if (initialState.isLyricRoute()) EnterTransition.None else slideInHorizontal(forward()) + }, + exitTransition = { + if (targetState.isLyricRoute()) ExitTransition.None else slideOutHorizontal(forward()) + }, + popEnterTransition = { + if (initialState.isLyricRoute()) EnterTransition.None else popEnterHorizontal() + }, + popExitTransition = { + if (targetState.isLyricRoute()) ExitTransition.None else popExitHorizontal() + }, + ) { + composable { HomeScreen(homeVm, profileVm, nav) } + composable { DiscoverScreen(homeVm, nav) } + composable { CreatorCenterScreen(creatorVm, nav) } + composable { MessagesScreen(messagesVm, nav) } + composable { ProfileScreen(profileVm, nav) } + + composable { RankingScreen(rankingVm, nav) } + composable { DailyScreen(dailyVm, nav) } + // 歌词页从下往上滑入,退出时滑回底部(对齐网页端的 translate-y 动画) + composable( + enterTransition = { + slideInVertically(animationSpec = tween(340)) { height -> height } }, - ), - ) { entry -> - ScanScreen( - initialTicket = entry.arguments?.getString("ticket"), - onBack = { navController.popBackStack() }, - onDone = { - navController.navigate(Routes.HOME) { - popUpTo(Routes.HOME) { inclusive = true } - } + popExitTransition = { + slideOutVertically(animationSpec = tween(300)) { height -> height } }, - ) + ) { + LyricScreen(lyricVm, playbackVm, nav) + } + composable { SearchScreen(searchVm, nav) } + composable { LatestSongsScreen(homeVm, nav) } + composable { NcmParseScreen(nav, playbackVm = playbackVm) } + composable { entry -> + SubmissionDetailScreen(entry.toRoute().id, creatorVm, nav) + } + composable { ProfileEditScreen(profileVm, nav) } + composable { ReviewQueueScreen(creatorVm, nav) } + composable { LyricSubmitScreen(nav) } + composable { DailySubmitScreen(nav, dailyVm) } + + composable { entry -> + ScanScreen( + initialTicket = entry.toRoute().ticket, + onBack = { navController.popBackStack() }, + onDone = { navController.popBackStack() }, + ) + } } } } -/** 读取本地登录态期间的启动占位,避免登录页一闪而过 */ +/** 17 启动页:图标 + 字标 + 标语 */ @Composable -private fun SplashPane() { - Surface(color = MaterialTheme.colorScheme.background) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), +private fun SplashScreen() { + Box( + Modifier.fillMaxSize().background(Amll.colors.canvas), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp), ) { - CircularProgressIndicator() + Image( + painter = painterResource(R.drawable.ic_launcher_logo), + contentDescription = null, + modifier = Modifier.size(84.dp).clip(RoundedCornerShape(20.dp)), + ) + Text("AMLL Hub", style = Font.pageTitle.copy(fontSize = 26.sp), color = Amll.colors.ink) + Text("让每一句歌词,都跟着唱出来", style = Font.caption, color = Amll.colors.ink3) } + Text( + "逐字歌词 · 社区共建", + style = Font.micro, + color = Amll.colors.ink3.copy(alpha = 0.7f), + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 34.dp), + ) } } diff --git a/android/app/src/main/java/dev/amll/hub/android/nav/AppRoutes.kt b/android/app/src/main/java/dev/amll/hub/android/nav/AppRoutes.kt new file mode 100644 index 0000000..5363a16 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/nav/AppRoutes.kt @@ -0,0 +1,97 @@ +package dev.amll.hub.android.nav + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * 类型安全路由(Navigation 2.8+):参数由序列化对象承载, + * 不再手写 `"scan?ticket={ticket}"` 这类字符串与 query 拼接。 + */ +@Serializable +@SerialName("home") +data object HomeRoute + +@Serializable +@SerialName("discover") +data object DiscoverRoute + +@Serializable +@SerialName("creator") +data object CreatorRoute + +@Serializable +@SerialName("messages") +data object MessagesRoute + +@Serializable +@SerialName("profile") +data object ProfileRoute + +@Serializable +@SerialName("ranking") +data object RankingRoute + +@Serializable +@SerialName("daily") +data object DailyRoute + +@Serializable +@SerialName("lyric") +data object LyricRoute + +@Serializable +@SerialName("search") +data object SearchRoute + +@Serializable +@SerialName("latest") +data object LatestRoute + +@Serializable +@SerialName("ncm") +data object NcmRoute + +@Serializable +@SerialName("submission") +data class SubmissionRoute(val id: Long) + +@Serializable +@SerialName("profile-edit") +data object ProfileEditRoute + +@Serializable +@SerialName("review") +data object ReviewRoute + +@Serializable +@SerialName("submit-lyric") +data object LyricSubmitRoute + +@Serializable +@SerialName("submit-daily") +data object DailySubmitRoute + +@Serializable +@SerialName("scan") +data class ScanRoute(val ticket: String? = null) + +/** 底栏 5 个 Tab,顺序即底栏下标 */ +val TabRoutes: List = listOf( + HomeRoute, + DiscoverRoute, + CreatorRoute, + MessagesRoute, + ProfileRoute, +) + +/** 与上面的路由 SerialName 一一对应,用于判断当前页是哪个 Tab */ +val TabRouteNames: List = listOf( + "home", + "discover", + "creator", + "messages", + "profile", +) + +/** 歌词页的 route 名,导航转场需要单独判断它 */ +val LyricRouteName: String = LyricRoute.serializer().descriptor.serialName diff --git a/android/app/src/main/java/dev/amll/hub/android/nav/NavTransitions.kt b/android/app/src/main/java/dev/amll/hub/android/nav/NavTransitions.kt new file mode 100644 index 0000000..fd22185 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/nav/NavTransitions.kt @@ -0,0 +1,28 @@ +package dev.amll.hub.android.nav + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.navigation.NavBackStackEntry + +/** + * 统一转场:推进时按 Tab 方向横向滑入,返回时反向; + * 歌词页用纵向转场(在 NavHost 里单独覆盖)。 + */ +fun AnimatedContentTransitionScope.slideInHorizontal(forward: Boolean): EnterTransition = + slideInHorizontally(animationSpec = tween(280)) { width -> if (forward) width else -width } + +fun AnimatedContentTransitionScope.slideOutHorizontal(forward: Boolean): ExitTransition = + slideOutHorizontally(animationSpec = tween(280)) { width -> if (forward) -width else width } + + fadeOut(animationSpec = tween(200)) + +fun AnimatedContentTransitionScope.popEnterHorizontal(): EnterTransition = + slideInHorizontally(animationSpec = tween(280)) { width -> -width } + +fun AnimatedContentTransitionScope.popExitHorizontal(): ExitTransition = + slideOutHorizontally(animationSpec = tween(280)) { width -> width } + + fadeOut(animationSpec = tween(200)) diff --git a/android/app/src/main/java/dev/amll/hub/android/playback/LyricLineParser.kt b/android/app/src/main/java/dev/amll/hub/android/playback/LyricLineParser.kt new file mode 100644 index 0000000..aca9272 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/playback/LyricLineParser.kt @@ -0,0 +1,107 @@ +package dev.amll.hub.android.playback + +data class TimedLyricLine(val timeMs: Long, val text: String) + +private val LRC_TAG = Regex("\\[(\\d{1,3}):(\\d{1,2})(?:[.:](\\d{1,3}))?]") + +private val TTML_P = Regex( + "]*)>(.*?)

", + setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE), +) +private val TTML_BEGIN = Regex("begin\\s*=\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE) +private val XML_TAG = Regex("<[^>]+>") +private val XML_ENTITY = Regex("&(#x?[0-9a-fA-F]+|lt|gt|quot|apos|amp);") + +/** + * 把歌词原文解析成带时间轴的行。 + * [format] 为 "ttml" 时按 TTML 解析,否则按 LRC 解析。 + */ +fun parseTimedLyric(text: String, format: String): List { + if (text.isBlank()) return emptyList() + return if (format.equals("ttml", ignoreCase = true)) parseTtml(text) else parseLrc(text) +} + +/** 当前应显示的一行:最后一个 timeMs <= positionMs 的文本 */ +fun currentLyricLine(lines: List, positionMs: Long, playing: Boolean): String? { + if (!playing || lines.isEmpty()) return null + return lines.lastOrNull { it.timeMs <= positionMs }?.text +} + +/** LRC:[mm:ss.xx] / [mm:ss],允许一行多个时间标签,跳过 [ar:] 等元信息与空行 */ +private fun parseLrc(text: String): List { + val result = ArrayList() + for (raw in text.lineSequence()) { + val tags = LRC_TAG.findAll(raw).toList() + if (tags.isEmpty()) continue + val content = raw.substring(tags.last().range.last + 1).trim() + if (content.isEmpty()) continue + for (tag in tags) { + val minutes = tag.groupValues[1].toLongOrNull() ?: continue + val seconds = tag.groupValues[2].toLongOrNull() ?: continue + val fraction = fractionMs(tag.groupValues[3]) + result += TimedLyricLine(minutes * 60_000 + seconds * 1_000 + fraction, content) + } + } + return result.sortedBy { it.timeMs } +} + +private fun fractionMs(raw: String): Long = when (raw.length) { + 0 -> 0L + 1 -> (raw.toLongOrNull() ?: 0L) * 100 + 2 -> (raw.toLongOrNull() ?: 0L) * 10 + else -> raw.take(3).toLongOrNull() ?: 0L +} + +/** TTML:

...

,begin 支持纯秒数与 hh:mm:ss.mmm */ +private fun parseTtml(text: String): List { + val result = ArrayList() + for (match in TTML_P.findAll(text)) { + val begin = TTML_BEGIN.find(match.groupValues[1])?.groupValues?.get(1) ?: continue + val timeMs = parseTtmlTime(begin) ?: continue + val plain = decodeXml(XML_TAG.replace(match.groupValues[2], "")).trim() + if (plain.isEmpty()) continue + result += TimedLyricLine(timeMs, plain) + } + return result.sortedBy { it.timeMs } +} + +private fun parseTtmlTime(raw: String): Long? { + val value = raw.trim() + value.toDoubleOrNull()?.let { seconds -> + if (seconds >= 0) return (seconds * 1000).toLong() + } + val parts = value.split(':') + if (parts.size !in 2..3) return null + val hours: Long + val minutes: Long + val seconds: Double + if (parts.size == 3) { + hours = parts[0].toLongOrNull() ?: return null + minutes = parts[1].toLongOrNull() ?: return null + seconds = parts[2].toDoubleOrNull() ?: return null + } else { + hours = 0L + minutes = parts[0].toLongOrNull() ?: return null + seconds = parts[1].toDoubleOrNull() ?: return null + } + return hours * 3_600_000 + minutes * 60_000 + (seconds * 1000).toLong() +} + +private fun decodeXml(value: String): String = XML_ENTITY.replace(value) { match -> + val entity = match.groupValues[1] + when (entity) { + "lt" -> "<" + "gt" -> ">" + "quot" -> "\"" + "apos" -> "'" + "amp" -> "&" + else -> runCatching { + val code = if (entity.startsWith("#x") || entity.startsWith("#X")) { + entity.drop(2).toInt(16) + } else { + entity.drop(1).toInt() + } + String(Character.toChars(code)) + }.getOrDefault(match.value) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/playback/NcmQuality.kt b/android/app/src/main/java/dev/amll/hub/android/playback/NcmQuality.kt new file mode 100644 index 0000000..a2a6be1 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/playback/NcmQuality.kt @@ -0,0 +1,99 @@ +package dev.amll.hub.android.playback + +/** 杜比全景声在移动端 ExoPlayer 上无法正常解码,播放前统一给出提示 */ +const val DOLBY_UNSUPPORTED_HINT = "杜比全景声暂不支持播放,可改选其它音质;或下载后用本地播放器收听" + +/** 网易云 8 档音质,顺序即档位高低(standard 最低、dolby 最高) */ +enum class NcmQuality(val key: String, val label: String) { + STANDARD("standard", "标准"), + EXHIGH("exhigh", "极高"), + LOSSLESS("lossless", "无损"), + HIRES("hires", "Hi-Res"), + JYEFFECT("jyeffect", "高清臻音"), + JYMASTER("jymaster", "超清母带"), + SKY("sky", "沉浸环绕声"), + DOLBY("dolby", "杜比全景声"), + ; + + companion object { + val ORDER: List = NcmQuality.entries + + /** key 与中文 label 都接,大小写不敏感 */ + fun fromKey(raw: String): NcmQuality? { + val value = raw.trim() + if (value.isEmpty()) return null + return ORDER.firstOrNull { + it.key.equals(value, ignoreCase = true) || it.label.equals(value, ignoreCase = true) + } + } + + /** 严格移植网页端 frontend/src/lib/quality.ts 的 normalizeNcmLevel */ + fun normalizeLevel(raw: String?): NcmQuality? { + val value = raw?.trim().orEmpty() + if (value.isEmpty()) return null + + val lower = value.lowercase() + if (lower in PLACEHOLDERS) return null + + fromKey(value)?.let { return it } + + EXACT_MAP[value]?.let { return it } + + qualityFromBitrate(value)?.let { return it } + + for ((pattern, quality) in KEYWORD_RULES) { + if (pattern.containsMatchIn(value)) return quality + } + return null + } + + /** 档位序号,越大越高;null / 未知返回 -1 */ + fun rank(q: NcmQuality?): Int = if (q == null) -1 else ORDER.indexOf(q) + + fun isDowngraded(actual: NcmQuality?, selected: NcmQuality): Boolean = + actual != null && rank(actual) < rank(selected) + } +} + +/** 上游 FormatLevel 的中文描述 */ +private val EXACT_MAP: Map = mapOf( + "标准" to NcmQuality.STANDARD, + "极高(HQ)" to NcmQuality.EXHIGH, + "无损(SQ)" to NcmQuality.LOSSLESS, + "高解析度无损(Hi-Res)" to NcmQuality.HIRES, + "高清臻音(Spatial Audio)" to NcmQuality.JYEFFECT, + "超清母带(Master)" to NcmQuality.JYMASTER, + "沉浸环绕声(Surround Audio)" to NcmQuality.SKY, + "杜比全景声(Dolby Atmos)" to NcmQuality.DOLBY, +) + +private val KEYWORD_RULES: List> = listOf( + Regex("杜比|dolby|atmos", RegexOption.IGNORE_CASE) to NcmQuality.DOLBY, + Regex("沉浸|环绕|surround", RegexOption.IGNORE_CASE) to NcmQuality.SKY, + Regex("母带|master", RegexOption.IGNORE_CASE) to NcmQuality.JYMASTER, + Regex("臻音|jyeffect|spatial", RegexOption.IGNORE_CASE) to NcmQuality.JYEFFECT, + Regex("hi-?res| hires|解析度", RegexOption.IGNORE_CASE) to NcmQuality.HIRES, + Regex("无损|lossless|\\bsq\\b|\\bflac\\b", RegexOption.IGNORE_CASE) to NcmQuality.LOSSLESS, + Regex("母带|jymaster", RegexOption.IGNORE_CASE) to NcmQuality.JYMASTER, + Regex("极高|exhigh|hq", RegexOption.IGNORE_CASE) to NcmQuality.EXHIGH, + Regex("标准|standard", RegexOption.IGNORE_CASE) to NcmQuality.STANDARD, +) + +/** 上游认不出来时的占位文案,出现即视为无效 */ +private val PLACEHOLDERS = setOf("未知音质", "未知", "unknown", "none", "null", "undefined") + +private val BITRATE = Regex("^(\\d{2,5})\\s*(k|kb|kbps|bps)$", RegexOption.IGNORE_CASE) + +/** 128k 附近归标准,320k 归极高,1000k+ 归无损 */ +private fun qualityFromBitrate(value: String): NcmQuality? { + val match = BITRATE.matchEntire(value) ?: return null + val digits = match.groupValues[1].toLongOrNull() ?: return null + val unit = match.groupValues[2].lowercase() + val kbps = if (unit == "bps") digits / 1000.0 else digits.toDouble() + if (kbps <= 0) return null + return when { + kbps <= 145 -> NcmQuality.STANDARD + kbps <= 345 -> NcmQuality.EXHIGH + else -> NcmQuality.LOSSLESS + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/playback/PlaybackService.kt b/android/app/src/main/java/dev/amll/hub/android/playback/PlaybackService.kt new file mode 100644 index 0000000..d86fe6a --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/playback/PlaybackService.kt @@ -0,0 +1,120 @@ +package dev.amll.hub.android.playback + +import android.app.PendingIntent +import android.content.Intent +import android.util.Log +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.DefaultMediaNotificationProvider +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import dagger.hilt.android.AndroidEntryPoint +import dev.amll.hub.android.MainActivity +import dev.amll.hub.android.R +import javax.inject.Inject + +/** + * 后台播放 + 系统媒体播控。 + * + * 通知栏、锁屏、蓝牙、耳机线控全部走 Media3 的 MediaSession: + * 系统从 `player.mediaMetadata` 取标题 / 歌手 / 封面,从 `player.playbackState` + * 取进度与播放态。所以 PlayerController 里必须给 MediaItem 带 metadata, + * 否则通知栏只会显示「未知曲目」。 + * + * 通知渠道定制(setMediaNotificationProvider / DefaultMediaNotificationProvider / + * ConnectionResult.AcceptedResultBuilder)在 media3 里都还标着 @UnstableApi, + * 这里整体 opt-in;类本身不标 @UnstableApi,避免把引用它的地方也带下水。 + */ +@androidx.annotation.OptIn(UnstableApi::class) +@AndroidEntryPoint +class PlaybackService : MediaSessionService() { + + @Inject + lateinit var playerController: PlayerController + + private var mediaSession: MediaSession? = null + + override fun onCreate() { + super.onCreate() + + val sessionActivity = PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + // 建 session 失败不应该把整个进程带走:没有系统播控也得能继续放歌 + val session = runCatching { + MediaSession.Builder(this, playerController.player) + .setSessionActivity(sessionActivity) + .setCallback(SessionCallback()) + .build() + }.onFailure { + Log.e(TAG, "create MediaSession failed", it) + }.getOrNull() + + if (session == null) { + // 没有 session 这个服务没有任何意义,直接收掉, + // 避免后续 bind 进来拿到 null + stopSelf() + return + } + mediaSession = session + + // 双播放器交叉淡化后会交换 active/standby,session 必须跟着换绑, + // 否则通知栏/锁屏拿到的还是已经停掉的旧 Player + playerController.onActivePlayerChanged = { newPlayer -> + runCatching { mediaSession?.setPlayer(newPlayer) } + .onFailure { Log.e(TAG, "switch media session player failed", it) } + } + + // 默认实现会自己建渠道,这里显式指定渠道名与通知图标, + // 让通知显示成「正在播放」而不是 media3 的默认英文渠道名 + runCatching { + setMediaNotificationProvider( + DefaultMediaNotificationProvider.Builder(this) + .setChannelId(CHANNEL_ID) + .setChannelName(R.string.playback_channel_name) + .setNotificationId(NOTIFICATION_ID) + .build() + .apply { setSmallIcon(R.drawable.ic_stat_music) }, + ) + }.onFailure { + Log.e(TAG, "set media notification provider failed", it) + } + } + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession + + /** 从最近任务里划掉:没在播就把服务收掉,还在播就继续留在通知栏 */ + override fun onTaskRemoved(rootIntent: Intent?) { + val player = playerController.player + if (!player.playWhenReady || player.mediaItemCount == 0) { + stopSelf() + } + } + + override fun onDestroy() { + // 先断开换绑回调,避免 session 释放后仍被切歌调用 + playerController.onActivePlayerChanged = null + // ExoPlayer 由 PlayerController 单例持有,session 只解绑不释放播放器 + mediaSession?.release() + mediaSession = null + playerController.onServiceDestroyed() + super.onDestroy() + } + + private inner class SessionCallback : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo, + ): MediaSession.ConnectionResult = + MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() + } + + private companion object { + const val TAG = "PlaybackService" + const val CHANNEL_ID = "amll_playback" + const val NOTIFICATION_ID = 1001 + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/playback/PlayerController.kt b/android/app/src/main/java/dev/amll/hub/android/playback/PlayerController.kt new file mode 100644 index 0000000..9d7b335 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/playback/PlayerController.kt @@ -0,0 +1,916 @@ +package dev.amll.hub.android.playback + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.amll.hub.android.data.remote.NcmMusicDto +import dev.amll.hub.android.data.repo.PlaybackRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random + +private const val TAG = "PlayerController" + +/** 自然播完续播的交叉淡化时长(与网页端一致) */ +private const val CROSSFADE_NATURAL_MS = 6_000L + +/** 手动切歌的快速淡化时长 */ +private const val CROSSFADE_MANUAL_MS = 2_000L + +/** 上一首自然结束后接预加载下一首的短淡化(旧轨已经没声了) */ +private const val AUTO_ADVANCE_FADE_MS = 300L + +/** 音量包络步进 */ +private const val CROSSFADE_STEP_MS = 50L + +/** 进度同步间隔 */ +private const val TICKER_MS = 500L + +/** 等待预加载曲目 ready 的最长时间 */ +private const val READY_TIMEOUT_MS = 10_000L + +/** 上一首的「回 0 而非切歌」阈值 */ +private const val PREV_RESTART_MS = 3_000L + +/** + * 播放内核:双 ExoPlayer 交替播放,实现队列、随机/循环与等功率交叉淡化。 + * + * active 播放当前曲目,standby 预加载下一首;交叉淡化时立即交换两者角色, + * 旧曲继续在后台播放到音量为 0 再清空。对外状态与网页端 PlayerBoot 对齐。 + */ +@Singleton +class PlayerController @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: PlaybackRepository, +) { + data class Track( + val songId: String, + val title: String, + val artist: String, + val coverUrl: String? = null, + val streamUrl: String? = null, + val albumName: String = "", + val lyricLrc: String = "", + val lyricTranslation: String = "", + val quality: NcmQuality = NcmQuality.EXHIGH, + val actualQuality: NcmQuality? = null, + ) + + data class QueueItem( + val songId: String, + val name: String, + val artists: String, + val coverUrl: String? = null, + val albumName: String = "", + ) + + enum class RepeatMode { OFF, ALL, ONE } + + data class State( + val track: Track? = null, + val playing: Boolean = false, + val positionMs: Long = 0L, + val durationMs: Long = 0L, + val buffering: Boolean = false, + val error: String? = null, + val resolving: Boolean = false, + val crossfading: Boolean = false, + val queue: List = emptyList(), + val currentIndex: Int = -1, + val shuffle: Boolean = false, + val repeatMode: RepeatMode = RepeatMode.OFF, + val quality: NcmQuality = NcmQuality.EXHIGH, + val actualQuality: NcmQuality? = null, + ) + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var ticker: Job? = null + + private val playerA: ExoPlayer = buildPlayer() + private val playerB: ExoPlayer = buildPlayer() + + @Volatile + private var active: ExoPlayer = playerA + private var standby: ExoPlayer = playerB + + /** 对外暴露当前 active 播放器;MediaSession 与 ViewModel 始终拿到正在响的那个 */ + val player: ExoPlayer get() = active + + /** active 切换后通知 MediaSession 换绑,必须在主线程调用 */ + var onActivePlayerChanged: ((Player) -> Unit)? = null + + private val _state = MutableStateFlow(State()) + val state: StateFlow = _state.asStateFlow() + + private var serviceStarted = false + + private var playRequestId = 0L + + private var plannedNextIndex = -1 + private var preloadedIndex = -1 + private var preloadedTrack: Track? = null + private var preloadToken = 0L + + private var crossfadeJob: Job? = null + private var outgoingPlayer: ExoPlayer? = null + + init { + playerA.addListener(listenerFor(playerA)) + playerB.addListener(listenerFor(playerB)) + } + + private fun buildPlayer(): ExoPlayer = ExoPlayer.Builder(context).build().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + true, + ) + // 拔耳机自动暂停,交给系统播控统一处理 + setHandleAudioBecomingNoisy(true) + // 息屏后保持 CPU 和网络唤醒,避免 Doze 挂起音频流 + setWakeMode(C.WAKE_MODE_NETWORK) + } + + /** + * Player.Listener 不带来源参数,这里按播放器各建一个实例, + * 所有回调统一 `if (p !== active) return`,避免 standby 的事件污染状态。 + */ + private fun listenerFor(p: ExoPlayer): Player.Listener = object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (p !== active) return + _state.update { it.copy(playing = isPlaying) } + if (isPlaying) startTicker() else stopTicker() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + if (p !== active) return + _state.update { it.copy(buffering = playbackState == Player.STATE_BUFFERING) } + if (playbackState == Player.STATE_READY) { + val duration = p.duration.takeIf { it != C.TIME_UNSET } ?: 0L + _state.update { it.copy(durationMs = duration) } + } + if (playbackState == Player.STATE_ENDED) handleEnded() + syncPosition() + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int, + ) { + if (p !== active) return + syncPosition() + } + + override fun onPlayerError(error: PlaybackException) { + if (p !== active) return + Log.w(TAG, "playback error: ${error.message}", error) + _state.update { it.copy(error = error.message, playing = false) } + } + } + + /* ===== 对外播放入口 ===== */ + + /** 已有解析结果的曲目直接播放;URL 为空时只更新信息并记录错误 */ + fun playResolved(track: Track, startPositionMs: Long = 0) { + abortCrossfade() + playRequestId++ + + val queue = _state.value.queue + val index = queue.indexOfFirst { it.songId == track.songId } + val playable = track.streamUrl?.takeIf { it.isNotBlank() } + + if (track.quality == NcmQuality.DOLBY) { + _state.update { + it.copy( + track = track, + currentIndex = index, + playing = false, + positionMs = 0L, + durationMs = 0L, + buffering = false, + error = DOLBY_UNSUPPORTED_HINT, + resolving = false, + crossfading = false, + actualQuality = track.actualQuality, + ) + } + stopAndClear(active) + clearPreload() + plannedNextIndex = -1 + return + } + + if (playable == null) { + _state.update { + it.copy( + track = track, + currentIndex = index, + playing = false, + positionMs = 0L, + durationMs = 0L, + buffering = false, + error = "该歌曲暂无音源", + resolving = false, + crossfading = false, + actualQuality = track.actualQuality, + ) + } + stopAndClear(active) + clearPreload() + plannedNextIndex = -1 + return + } + + startPlaybackService() + _state.update { + it.copy( + track = track, + currentIndex = index, + playing = true, + positionMs = startPositionMs, + durationMs = 0L, + buffering = true, + error = null, + resolving = false, + crossfading = false, + actualQuality = track.actualQuality, + ) + } + + clearPreload() + active.setMediaItem(buildMediaItem(track, Uri.parse(playable))) + active.prepare() + active.seekTo(startPositionMs) + active.play() + startTicker() + schedulePreload() + } + + /** 解析并播放单曲,不改变当前队列 */ + fun playSong( + songId: String, + meta: QueueItem, + quality: NcmQuality = _state.value.quality, + ) { + if (songId.isBlank()) return + abortCrossfade() + clearPreload() + plannedNextIndex = -1 + + if (quality == NcmQuality.DOLBY) { + _state.update { + it.copy( + playing = false, + error = DOLBY_UNSUPPORTED_HINT, + resolving = false, + quality = quality, + ) + } + stopAndClear(active) + return + } + + val requestId = ++playRequestId + _state.update { it.copy(resolving = true, error = null, quality = quality) } + scope.launch { + val resolved = repository.resolve(songId, quality.key) + if (requestId != playRequestId) return@launch + val music = resolved.music + val track = Track( + songId = songId, + title = music?.displayName?.takeIf { it.isNotBlank() } + ?: meta.name.ifBlank { "未知歌曲" }, + artist = music?.displayArtist?.takeIf { it.isNotBlank() } + ?: meta.artists.ifBlank { "未知歌手" }, + coverUrl = music?.displayCover?.takeIf { it.isNotBlank() } ?: meta.coverUrl, + streamUrl = music?.displayUrl?.takeIf { it.isNotBlank() }, + albumName = music?.albumName?.takeIf { it.isNotBlank() } ?: meta.albumName, + lyricLrc = music?.lyric.orEmpty(), + lyricTranslation = music?.tlyric.orEmpty(), + quality = quality, + actualQuality = resolved.actual, + ) + if (track.streamUrl == null) { + Log.w(TAG, "playSong $songId no url: ${resolved.error}") + } + playResolved(track) + } + } + + /** 替换队列并从指定项开始播放 */ + fun playQueue( + items: List, + startIndex: Int = 0, + quality: NcmQuality = _state.value.quality, + ) { + if (items.isEmpty()) return + val index = startIndex.coerceIn(0, items.lastIndex) + abortCrossfade() + _state.update { + it.copy(queue = items, currentIndex = index, quality = quality, error = null) + } + playIndex(index) + } + + fun playAt(index: Int) { + if (index !in _state.value.queue.indices) return + playIndex(index) + } + + fun next(auto: Boolean = false) { + if (crossfadeJob != null) abortCrossfade() + val snapshot = _state.value + val queue = snapshot.queue + if (queue.isEmpty()) { + if (auto) _state.update { it.copy(playing = false) } + return + } + + if (queue.size == 1) { + if (snapshot.repeatMode == RepeatMode.ALL) { + active.seekTo(0) + active.play() + startTicker() + } else if (auto) { + _state.update { it.copy(playing = false) } + } + return + } + + val current = snapshot.currentIndex + var nextIndex: Int + if (plannedNextIndex in queue.indices) { + nextIndex = plannedNextIndex + } else if (snapshot.shuffle) { + nextIndex = randomOther(queue.size, current) + } else { + var candidate = if (current in queue.indices) current + 1 else 0 + if (candidate >= queue.size) { + if (snapshot.repeatMode == RepeatMode.ALL) { + candidate = 0 + } else { + if (auto) _state.update { it.copy(playing = false) } + return + } + } + nextIndex = candidate + } + val preloadedReady = auto && + preloadedTrack != null && + preloadedIndex == nextIndex && + plannedNextIndex == nextIndex && + standby.mediaItemCount > 0 && + standby.playbackState == Player.STATE_READY + if (preloadedReady) { + // 一曲自然播完时旧轨已经结束,用短淡化直接接到预加载好的下一首, + // 不再重新走一次网络解析 + startCrossfade(nextIndex, AUTO_ADVANCE_FADE_MS) + return + } + playIndex(nextIndex, crossfadeMs = CROSSFADE_MANUAL_MS, allowCrossfade = !auto) + } + + fun prev() { + if (crossfadeJob != null) abortCrossfade() + val snapshot = _state.value + val queue = snapshot.queue + if (queue.isEmpty()) return + if (snapshot.positionMs > PREV_RESTART_MS || queue.size == 1) { + active.seekTo(0) + return + } + val current = snapshot.currentIndex + val prevIndex = if (snapshot.shuffle) { + randomOther(queue.size, current) + } else { + var candidate = if (current in queue.indices) current - 1 else 0 + if (candidate < 0) { + candidate = if (snapshot.repeatMode == RepeatMode.ALL) queue.lastIndex else 0 + } + candidate + } + playIndex(prevIndex) + } + + fun toggle() { + if (crossfadeJob != null) abortCrossfade() + if (player.isPlaying) { + player.pause() + } else { + // 循环 OFF 播到队尾后停在 ENDED,再按播放要从头重播 + if (player.playbackState == Player.STATE_ENDED) player.seekTo(0) + if (player.mediaItemCount > 0) startPlaybackService() + player.play() + startTicker() + } + } + + fun pause() { + player.pause() + } + + fun seekTo(positionMs: Long) { + if (crossfadeJob != null) abortCrossfade() + player.seekTo(positionMs.coerceAtLeast(0)) + // 立即回写,不等 onPositionDiscontinuity,拖动条松手时无延迟 + syncPosition() + } + + fun seekBy(deltaMs: Long) = seekTo(player.currentPosition + deltaMs) + + fun toggleShuffle() { + _state.update { it.copy(shuffle = !it.shuffle) } + clearPreload() + schedulePreload() + } + + fun cycleRepeatMode() { + _state.update { + val next = when (it.repeatMode) { + RepeatMode.OFF -> RepeatMode.ALL + RepeatMode.ALL -> RepeatMode.ONE + RepeatMode.ONE -> RepeatMode.OFF + } + it.copy(repeatMode = next) + } + clearPreload() + schedulePreload() + } + + /** 只更新后续播放使用的音质,不重载当前曲目 */ + fun setQuality(quality: NcmQuality) { + if (_state.value.quality == quality) return + _state.update { it.copy(quality = quality) } + clearPreload() + schedulePreload() + } + + fun removeFromQueue(index: Int) { + val snapshot = _state.value + if (index !in snapshot.queue.indices) return + val queue = snapshot.queue.toMutableList().apply { removeAt(index) } + val current = snapshot.currentIndex + val newIndex = when { + index == current -> -1 + index < current -> current - 1 + else -> current + } + _state.update { it.copy(queue = queue, currentIndex = newIndex) } + clearPreload() + schedulePreload() + } + + fun clearQueue() { + _state.update { it.copy(queue = emptyList(), currentIndex = -1) } + clearPreload() + plannedNextIndex = -1 + } + + /** 当前曲目无可用流时用其原音质重新解析 */ + fun replayCurrent() { + val track = _state.value.track ?: return + abortCrossfade() + clearPreload() + plannedNextIndex = -1 + val requestId = ++playRequestId + _state.update { it.copy(resolving = true, error = null) } + scope.launch { + val resolved = repository.resolve(track.songId, track.quality.key) + if (requestId != playRequestId) return@launch + val music = resolved.music + val newTrack = track.copy( + title = music?.displayName?.takeIf { it.isNotBlank() } ?: track.title, + artist = music?.displayArtist?.takeIf { it.isNotBlank() } ?: track.artist, + coverUrl = music?.displayCover?.takeIf { it.isNotBlank() } ?: track.coverUrl, + streamUrl = music?.displayUrl?.takeIf { it.isNotBlank() }, + albumName = music?.albumName?.takeIf { it.isNotBlank() } ?: track.albumName, + lyricLrc = music?.lyric.orEmpty().ifBlank { track.lyricLrc }, + lyricTranslation = music?.tlyric.orEmpty().ifBlank { track.lyricTranslation }, + actualQuality = resolved.actual, + ) + if (newTrack.streamUrl == null) { + Log.w(TAG, "replayCurrent ${track.songId} no url: ${resolved.error}") + } + playResolved(newTrack) + } + } + + /** PlaybackService 销毁时回调,下次播放要重新拉起服务 */ + fun onServiceDestroyed() { + serviceStarted = false + } + + fun release() { + abortCrossfade() + stopTicker() + preloadToken++ + onActivePlayerChanged = null + scope.cancel() + playerA.release() + playerB.release() + } + + /* ===== 队列内切歌与交叉淡化 ===== */ + + /** + * 播放队列中指定项。目标若是已预加载的下一首且 standby 就绪, + * 直接淡化切换;否则解析到 standby 等 READY 再淡化,失败回退硬切。 + */ + private fun playIndex( + index: Int, + crossfadeMs: Long = CROSSFADE_MANUAL_MS, + allowCrossfade: Boolean = true, + ) { + val snapshot = _state.value + if (index !in snapshot.queue.indices) return + if (crossfadeJob != null) abortCrossfade() + + val target = snapshot.queue[index] + val preloadedReady = preloadedTrack != null && + preloadedIndex == index && + plannedNextIndex == index && + preloadedTrack?.songId == target.songId && + standby.mediaItemCount > 0 && + standby.playbackState == Player.STATE_READY + if (allowCrossfade && preloadedReady) { + startCrossfade(index, crossfadeMs) + return + } + + val quality = snapshot.quality + if (quality == NcmQuality.DOLBY) { + _state.update { + it.copy( + currentIndex = index, + playing = false, + error = DOLBY_UNSUPPORTED_HINT, + resolving = false, + ) + } + stopAndClear(active) + clearPreload() + plannedNextIndex = -1 + return + } + + val requestId = ++playRequestId + clearPreload() + plannedNextIndex = -1 + _state.update { it.copy(currentIndex = index, resolving = true, error = null) } + scope.launch { + val resolved = repository.resolve(target.songId, quality.key) + if (requestId != playRequestId) return@launch + val music = resolved.music + val url = music?.displayUrl?.takeIf { it.isNotBlank() } + if (url == null) { + Log.w(TAG, "playIndex ${target.songId} no url: ${resolved.error}") + _state.update { + it.copy( + currentIndex = index, + playing = false, + resolving = false, + error = resolved.error ?: "该歌曲暂无音源", + ) + } + stopAndClear(active) + return@launch + } + + val track = toTrack(target, music, quality, resolved.actual) + // 当前没有任何可播内容(首次播放/上次失败)时没有旧轨可淡化,直接开始 + if (!allowCrossfade || active.mediaItemCount == 0) { + playResolved(track) + return@launch + } + + standby.volume = 0f + standby.setMediaItem(buildMediaItem(track, Uri.parse(url))) + standby.prepare() + val ready = waitForReady(standby, READY_TIMEOUT_MS) + if (requestId != playRequestId) return@launch + if (!ready || crossfadeJob != null) { + stopAndClear(standby) + playResolved(track) + return@launch + } + preloadedIndex = index + preloadedTrack = track + startCrossfade(index, crossfadeMs) + } + } + + /** + * 等功率交叉淡化:旧轨 cos(t·π/2)、新轨 sin(t·π/2), + * 开始时立即把 UI 与 MediaSession 切到新曲,结束时清空旧轨。 + */ + private fun startCrossfade(index: Int, durationMs: Long) { + if (crossfadeJob != null) abortCrossfade() + val track = preloadedTrack ?: return + if (standby.playbackState != Player.STATE_READY) return + // 服务可能被系统回收过,换绑 session 前先确保它还在 + startPlaybackService() + + val outgoing = active + val incoming = standby + preloadedIndex = -1 + preloadedTrack = null + // 旧曲的「下一首」就是正在淡入的这首,不能再作为下一次切歌目标 + plannedNextIndex = -1 + preloadToken++ + + incoming.volume = 0f + incoming.play() + active = incoming + standby = outgoing + outgoingPlayer = outgoing + onActivePlayerChanged?.invoke(active) + + val duration = incoming.duration.takeIf { it != C.TIME_UNSET } ?: 0L + _state.update { + it.copy( + track = track, + currentIndex = index, + playing = true, + positionMs = incoming.currentPosition.coerceAtLeast(0L), + durationMs = duration, + buffering = false, + error = null, + resolving = false, + crossfading = true, + actualQuality = track.actualQuality, + ) + } + startTicker() + + val steps = (durationMs / CROSSFADE_STEP_MS).toInt().coerceAtLeast(1) + crossfadeJob = scope.launch { + for (step in 0..steps) { + val t = step.toFloat() / steps + val angle = t * (Math.PI.toFloat() / 2) + outgoing.volume = cos(angle) + incoming.volume = sin(angle) + delay(CROSSFADE_STEP_MS) + } + outgoing.pause() + outgoing.clearMediaItems() + outgoing.volume = 1f + incoming.volume = 1f + outgoingPlayer = null + crossfadeJob = null + _state.update { it.copy(crossfading = false) } + schedulePreload() + } + } + + /** 立即中止淡化:取消协程、停掉旧轨并复位音量 */ + private fun abortCrossfade() { + val job = crossfadeJob + crossfadeJob = null + job?.cancel() + val outgoing = outgoingPlayer + outgoingPlayer = null + if (outgoing != null && outgoing !== active) { + outgoing.pause() + outgoing.clearMediaItems() + outgoing.volume = 1f + } + active.volume = 1f + if (_state.value.crossfading) _state.update { it.copy(crossfading = false) } + } + + /* ===== 下一首预加载 ===== */ + + /** 重算预定下一首并把结果缓冲到 standby(不播放) */ + private fun schedulePreload() { + if (crossfadeJob != null) return + val snapshot = _state.value + plannedNextIndex = computePlannedNext(snapshot) + preloadToken++ + preloadedIndex = -1 + preloadedTrack = null + + val index = plannedNextIndex + if (index !in snapshot.queue.indices) return + if (snapshot.repeatMode == RepeatMode.ONE) return + if (snapshot.quality == NcmQuality.DOLBY) return + + val target = snapshot.queue[index] + val quality = snapshot.quality + val token = preloadToken + scope.launch { + val resolved = repository.resolve(target.songId, quality.key) + if (token != preloadToken) return@launch + val music = resolved.music + val url = music?.displayUrl?.takeIf { it.isNotBlank() } + if (url == null) return@launch + if (crossfadeJob != null) return@launch + val track = toTrack(target, music, quality, resolved.actual) + standby.volume = 0f + standby.setMediaItem(buildMediaItem(track, Uri.parse(url))) + standby.prepare() + preloadedIndex = index + preloadedTrack = track + } + } + + private fun computePlannedNext(snapshot: State): Int { + val queue = snapshot.queue + val current = snapshot.currentIndex + if (queue.size <= 1 || current !in queue.indices) return -1 + if (snapshot.repeatMode == RepeatMode.ONE) return -1 + if (snapshot.quality == NcmQuality.DOLBY) return -1 + return if (snapshot.shuffle) { + randomOther(queue.size, current) + } else { + val candidate = current + 1 + if (candidate >= queue.size) { + if (snapshot.repeatMode == RepeatMode.ALL) 0 else -1 + } else { + candidate + } + } + } + + private fun clearPreload() { + preloadToken++ + preloadedIndex = -1 + preloadedTrack = null + if (crossfadeJob != null) return + stopAndClear(standby) + } + + private fun randomOther(size: Int, current: Int): Int { + if (size <= 1) return 0 + var index: Int + do { + index = Random.nextInt(size) + } while (index == current) + return index + } + + /* ===== 播放器事件与进度 ===== */ + + private fun handleEnded() { + if (crossfadeJob != null) return + if (_state.value.repeatMode == RepeatMode.ONE) { + // 单曲循环:不淡化,原地重播 + active.seekTo(0) + active.play() + startTicker() + return + } + next(auto = true) + } + + /** ticker 每 500ms 校正进度,并在临近结尾时触发自然交叉淡化 */ + private fun checkNaturalCrossfade() { + if (crossfadeJob != null) return + val snapshot = _state.value + if (snapshot.repeatMode == RepeatMode.ONE) return + val planned = plannedNextIndex + if (planned < 0 || planned != preloadedIndex) return + if (preloadedTrack == null) return + if (snapshot.durationMs <= 0L) return + if (snapshot.durationMs - snapshot.positionMs > CROSSFADE_NATURAL_MS) return + if (standby.mediaItemCount == 0) return + if (standby.playbackState != Player.STATE_READY) return + startCrossfade(planned, CROSSFADE_NATURAL_MS) + } + + private fun syncPosition() { + _state.update { it.copy(positionMs = active.currentPosition.coerceAtLeast(0)) } + } + + private fun startTicker() { + if (ticker?.isActive == true) return + ticker = scope.launch { + while (isActive) { + syncPosition() + checkNaturalCrossfade() + delay(TICKER_MS) + } + } + } + + private fun stopTicker() { + ticker?.cancel() + ticker = null + } + + /* ===== 工具 ===== */ + + private fun stopAndClear(target: ExoPlayer) { + target.stop() + target.clearMediaItems() + target.volume = 1f + } + + private fun toTrack( + item: QueueItem, + music: NcmMusicDto?, + quality: NcmQuality, + actual: NcmQuality?, + ): Track = Track( + songId = item.songId, + title = music?.displayName?.takeIf { it.isNotBlank() } + ?: item.name.ifBlank { "未知歌曲" }, + artist = music?.displayArtist?.takeIf { it.isNotBlank() } + ?: item.artists.ifBlank { "未知歌手" }, + coverUrl = music?.displayCover?.takeIf { it.isNotBlank() } ?: item.coverUrl, + streamUrl = music?.displayUrl?.takeIf { it.isNotBlank() }, + albumName = music?.albumName?.takeIf { it.isNotBlank() } ?: item.albumName, + lyricLrc = music?.lyric.orEmpty(), + lyricTranslation = music?.tlyric.orEmpty(), + quality = quality, + actualQuality = actual, + ) + + private suspend fun waitForReady(target: ExoPlayer, timeoutMs: Long): Boolean = + withTimeoutOrNull(timeoutMs) { + if (target.playbackState == Player.STATE_READY) return@withTimeoutOrNull true + suspendCancellableCoroutine { continuation -> + val listener = object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY && continuation.isActive) { + target.removeListener(this) + continuation.resume(true) + } + } + + override fun onPlayerError(error: PlaybackException) { + if (continuation.isActive) { + target.removeListener(this) + continuation.resume(false) + } + } + } + target.addListener(listener) + continuation.invokeOnCancellation { target.removeListener(listener) } + } + } ?: false + + private fun startPlaybackService() { + if (serviceStarted) return + val intent = Intent(context, PlaybackService::class.java) + try { + context.startService(intent) + serviceStarted = true + } catch (e: IllegalStateException) { + Log.w(TAG, "startService rejected, playback continues without media session", e) + } + } + + /** + * 通知栏 / 锁屏 / 蓝牙显示的信息都来自 MediaItem 的 metadata。 + * uri 不允许 null:无 URI 的 MediaItem 会让 ExoPlayer 在创建 MediaSource 时崩溃。 + */ + private fun buildMediaItem(track: Track, uri: Uri): MediaItem { + val metadata = MediaMetadata.Builder() + .setTitle(track.title.ifBlank { "未知歌曲" }) + .setArtist(track.artist.ifBlank { "未知歌手" }) + .setAlbumTitle(track.albumName.ifBlank { "AMLL Hub" }) + .setIsBrowsable(false) + .setIsPlayable(true) + .apply { + track.coverUrl?.takeIf { it.isNotBlank() }?.let { + runCatching { setArtworkUri(Uri.parse(it)) } + } + } + .build() + + return MediaItem.Builder() + .setMediaId(track.songId) + .setUri(uri) + .setMediaMetadata(metadata) + .build() + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/AppNav.kt b/android/app/src/main/java/dev/amll/hub/android/ui/AppNav.kt new file mode 100644 index 0000000..28699b5 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/AppNav.kt @@ -0,0 +1,97 @@ +package dev.amll.hub.android.ui + +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import dev.amll.hub.android.nav.DailyRoute +import dev.amll.hub.android.nav.DailySubmitRoute +import dev.amll.hub.android.nav.LatestRoute +import dev.amll.hub.android.nav.LyricRoute +import dev.amll.hub.android.nav.LyricSubmitRoute +import dev.amll.hub.android.nav.NcmRoute +import dev.amll.hub.android.nav.ProfileEditRoute +import dev.amll.hub.android.nav.RankingRoute +import dev.amll.hub.android.nav.ReviewRoute +import dev.amll.hub.android.nav.ScanRoute +import dev.amll.hub.android.nav.SearchRoute +import dev.amll.hub.android.nav.SubmissionRoute +import dev.amll.hub.android.nav.TabRoutes + +/** + * 页面跳转集合:统一封装 [NavController],路由用类型安全对象。 + * + * 成员保持「函数值」形式,这样调用方既能 `nav.back()` 也能 `onBack = nav.back`。 + */ +class AppNav( + private val navController: NavController, + /** 打开歌词前的副作用:解析音源 + 拉歌词,由 NavGraph 注入 */ + onOpenTrack: ( + songId: String, + title: String, + artist: String, + cover: String?, + rawLyricFile: String?, + ) -> Unit = { _, _, _, _, _ -> }, + /** 从播放条进歌词页:补一次当前曲目的歌词 */ + onPlayerLyric: () -> Unit = {}, +) { + + val back: () -> Unit = { navController.popBackStack() } + + val openRanking: () -> Unit = { navController.navigate(RankingRoute) } + val openDaily: () -> Unit = { navController.navigate(DailyRoute) } + val openSearch: () -> Unit = { navController.navigate(SearchRoute) } + val openLatest: () -> Unit = { navController.navigate(LatestRoute) } + + /** 网易云解析:粘 ID / 链接 或 关键词搜索后直接播放 */ + val openNcm: () -> Unit = { navController.navigate(NcmRoute) } + val openScan: () -> Unit = { navController.navigate(ScanRoute()) } + + val openLyric: (songId: String, title: String, artist: String, cover: String?) -> Unit = + { songId, title, artist, cover -> + onOpenTrack(songId, title, artist, cover, null) + navController.navigate(LyricRoute) + } + + /** 搜索命中的歌词:多带一个词库 TTML 文件名,优先于 ncm-lyrics 目录 */ + val openLyricFile: ( + songId: String, + title: String, + artist: String, + cover: String?, + rawLyricFile: String?, + ) -> Unit = { songId, title, artist, cover, rawLyricFile -> + onOpenTrack(songId, title, artist, cover, rawLyricFile) + navController.navigate(LyricRoute) + } + + /** 直接打开当前播放曲目的歌词页(迷你播放器用) */ + val openPlayer: () -> Unit = { + onPlayerLyric() + navController.navigate(LyricRoute) + } + + val openSubmission: (Long) -> Unit = { id -> navController.navigate(SubmissionRoute(id)) } + val openProfileEdit: () -> Unit = { navController.navigate(ProfileEditRoute) } + val openReview: () -> Unit = { navController.navigate(ReviewRoute) } + + /** 创作中心投稿入口 */ + val openSubmitLyric: () -> Unit = { navController.navigate(LyricSubmitRoute) } + val openSubmitDaily: () -> Unit = { navController.navigate(DailySubmitRoute) } + + /** 每日推荐投稿成功:关掉投稿页并跳到每日推荐(由 DailyViewModel 指定日期) */ + val finishSubmitToDaily: () -> Unit = { + navController.popBackStack() + navController.navigate(DailyRoute) { launchSingleTop = true } + } + + val switchTab: (Int) -> Unit = { index -> + TabRoutes.getOrNull(index)?.let { route -> + navController.navigate(route) { + // 每个 Tab 各自保留返回栈与状态,切回来不重建 + popUpTo(navController.graph.findStartDestination().id) { saveState = true } + launchSingleTop = true + restoreState = true + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/AppShell.kt b/android/app/src/main/java/dev/amll/hub/android/ui/AppShell.kt new file mode 100644 index 0000000..a62a386 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/AppShell.kt @@ -0,0 +1,184 @@ +package dev.amll.hub.android.ui + +import android.os.Build +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.playback.currentLyricLine +import dev.amll.hub.android.playback.parseTimedLyric +import dev.amll.hub.android.ui.component.MiniPlayer +import dev.amll.hub.android.ui.component.PillTabBar +import dev.amll.hub.android.ui.component.PlaylistSheet +import dev.amll.hub.android.ui.screen.AppTabs +import dev.amll.hub.android.ui.screen.LyricViewModel +import dev.amll.hub.android.ui.screen.PlaybackViewModel +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Size +import dev.chrisbanes.haze.HazeInput +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.blur.HazeBlurStyle +import dev.chrisbanes.haze.blur.hazeBlur +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState + +/** 播放条高度:54dp 信息行 + 2dp 进度线 */ +private val MiniPlayerHeight = 56.dp + +/** 胶囊导航占用的高度(不含系统导航栏):8dp 上边距 + 胶囊 + 18dp 下边距 */ +private val TabBarHeight = 8.dp + Size.tabPill + 18.dp + +/** + * 底部栏在内容区需要额外预留的高度。 + * + * 底栏是浮在内容之上的,列表要能滚动到最后一个条目,所以各页的滚动容器 + * 用这个值补一个底部内边距;这样中间的内容依然会从底栏后面穿过去, + * 模糊才有东西可糊。 + */ +val LocalBottomBarSpace = compositionLocalOf { 0.dp } + +/** + * App 外壳:内容区 + 常驻底部栏。 + * + * 底栏放在 NavHost 外面,切页时只有内容滑动,栏本身不动; + * 进二级页时栏整体淡出(与网页端一致)。 + */ +@Composable +fun AppShell( + playerVm: PlaybackViewModel, + lyricVm: LyricViewModel, + nav: AppNav, + selectedTab: Int, + tabsVisible: Boolean, + content: @Composable () -> Unit, +) { + val player by playerVm.playerState.collectAsStateWithLifecycle() + val lyric by lyricVm.lyric.collectAsStateWithLifecycle() + val navBarBottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + // 只显示与当前播放曲目匹配的歌词:从解析页直接播放时 LyricViewModel + // 还停留在上一首,不能把旧歌词显示到迷你播放器上。 + val lyricMatches = lyric.songId.isNotBlank() && lyric.songId == player.track?.songId + val lyricLines = remember(lyric.source.text, lyric.source.format, lyricMatches) { + if (lyricMatches) parseTimedLyric(lyric.source.text, lyric.source.format) else emptyList() + } + val lyricLine = currentLyricLine(lyricLines, player.positionMs, player.playing) + var showPlaylist by rememberSaveable { mutableStateOf(false) } + + val hasTrack = player.track != null + val bottomSpace = if (tabsVisible) { + TabBarHeight + navBarBottom + if (hasTrack) MiniPlayerHeight else 0.dp + } else { + 0.dp + } + + val hazeState: HazeState = rememberHazeState() + // 底色用卡片色(网页端顶栏是 bg-card/80,不是页面底色)。 + // 底栏自己还有一层 0.97 的实心底兜底(haze 在源内容静止时会跳过绘制), + // 所以这里的透明度可以低一些,让模糊在滚动时更明显 + val blurSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val tint = Amll.colors.surface.copy(alpha = if (blurSupported) 0.72f else 0.98f) + val blurStyle = remember(tint) { + HazeBlurStyle { + blurRadius(36.dp) + backgroundColor(tint) + } + } + + CompositionLocalProvider(LocalBottomBarSpace provides bottomSpace) { + Box(Modifier.fillMaxSize().background(Amll.colors.canvas)) { + // 内容铺满整屏(含底栏区域),haze 从这里采样 + Box(Modifier.fillMaxSize().hazeSource(state = hazeState)) { + content() + } + + AnimatedVisibility( + visible = tabsVisible, + modifier = Modifier.align(Alignment.BottomCenter), + enter = fadeIn(tween(160)), + exit = fadeOut(tween(120)), + ) { + Column(Modifier.fillMaxWidth()) { + MiniPlayer( + state = player, + loading = lyric.loading || player.resolving, + error = player.error, + lyricLine = lyricLine, + onOpen = nav.openPlayer, + onToggle = playerVm::togglePlay, + onTogglePlaylist = { showPlaylist = true }, + modifier = Modifier.hazeBlur( + input = HazeInput.Sources(state = hazeState), + style = blurStyle, + ), + ) + PillTabBar( + items = AppTabs, + selectedIndex = selectedTab.coerceAtLeast(0), + onSelect = nav.switchTab, + modifier = Modifier.hazeBlur( + input = HazeInput.Sources(state = hazeState), + style = blurStyle, + ), + ) + } + } + + if (showPlaylist && player.track != null) { + PlaylistSheet( + state = player, + onDismiss = { showPlaylist = false }, + onPlayAt = { + playerVm.playAt(it) + showPlaylist = false + }, + onRemove = playerVm::removeAt, + onClear = playerVm::clearQueue, + ) + } + } + } +} + +/** + * Tab 页的公共容器:只负责状态栏避让。 + * + * 底部内边距由各页通过 LocalBottomBarSpace 补给滚动容器, + * 这里不能直接 padding 掉,否则内容就不会从底栏后面滑过了。 + */ +@Composable +fun TabScaffold(content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding(), + content = content, + ) +} + +/** Tab 页滚动容器的底部内边距:栏高 + 页面自身留白 */ +@Composable +fun bottomBarPadding(extra: Dp = 18.dp): Dp = LocalBottomBarSpace.current + extra diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricPreview.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricPreview.kt new file mode 100644 index 0000000..b310c47 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricPreview.kt @@ -0,0 +1,54 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Radius + +/** + * 解析页的歌词预览:固定高度内复用 AMLL 歌词页,只负责渲染, + * 播放控制全部为空操作,避免预览误控当前正在播放的曲目。 + */ +@Composable +fun AmllLyricPreview( + rawLyric: String, + lyricFormat: String, + lyricTranslation: String, + title: String, + artist: String, + cover: String?, + modifier: Modifier = Modifier, +) { + Box( + modifier + .fillMaxWidth() + .height(320.dp) + .clip(RoundedCornerShape(Radius.card)) + .background(Amll.colors.surface2), + ) { + AmllLyricView( + rawLyric = rawLyric, + lyricFormat = lyricFormat, + lyricTranslation = lyricTranslation, + positionMs = 0, + playing = false, + title = title, + artist = artist, + durationMs = 0, + cover = cover, + dark = false, + onTogglePlay = {}, + onSeek = {}, + onClose = {}, + modifier = Modifier.fillMaxSize(), + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricView.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricView.kt new file mode 100644 index 0000000..07d371a --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/AmllLyricView.kt @@ -0,0 +1,330 @@ +package dev.amll.hub.android.ui.component + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.ViewGroup +import android.webkit.ConsoleMessage +import android.webkit.JavascriptInterface +import android.webkit.RenderProcessGoneDetail +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.viewinterop.AndroidView +import androidx.webkit.WebViewAssetLoader +import dev.amll.hub.android.BuildConfig +import org.json.JSONObject + +private const val TAG = "AmllLyricView" + +/** + * 歌词页实例跨导航保留。 + * + * 之前 WebView 跟着 LyricScreen 的 composition 一起销毁,每次进歌词页都要 + * 重新加载页面、重新解析歌词,视觉上会闪一下。这里把实例缓存在进程级, + * 重新进入时直接复用并在 composition 恢复后补推当前状态。 + */ +private object LyricPageState { + val webView = mutableStateOf(null) + + /** 页面 onPageFinished 之后才允许推数据 */ + val loaded = mutableStateOf(false) + + /** 渲染进程崩溃后换一个新实例,AndroidView 靠它重建 */ + val restart = mutableIntStateOf(0) + + /** 当前 composition 的命令处理器,页面 → 原生 都走这里 */ + val onCommand = mutableStateOf<((String, Double?) -> Unit)?>(null) +} + +/** + * 内置 AMLL 歌词页(assets/lyrics,AMLL core 构建产物)。 + * + * 页面与网页端 LyricsPage 一致,只负责渲染与交互;时间轴由原生播放器驱动, + * 页面上的播放/暂停、点歌词跳转、顶部拖动条退出、菜单按钮都通过 JS 接口回传。 + */ +@Composable +fun AmllLyricView( + /** 原始歌词文本,由页面用 AMLL 官方解析器解析(与网页端同款库) */ + rawLyric: String, + /** "ttml" / "lrc" */ + lyricFormat: String, + /** LRC 的翻译文本(tlyric) */ + lyricTranslation: String, + positionMs: Long, + playing: Boolean, + title: String, + artist: String, + durationMs: Long, + cover: String?, + dark: Boolean, + onTogglePlay: () -> Unit, + onSeek: (Long) -> Unit, + onClose: () -> Unit, + onNext: () -> Unit = {}, + onPrev: () -> Unit = {}, + onToggleShuffle: () -> Unit = {}, + onCycleRepeat: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + val webView = LyricPageState.webView.value + val loaded = LyricPageState.loaded.value + val coverUrl = remember(cover) { cover.orEmpty() } + val mainHandler = remember { Handler(Looper.getMainLooper()) } + + // edge-to-edge:页面背景要铺到状态栏/导航栏后面,内容再按这两个高度内缩。 + // WebView 里 CSS px 约等于 dp(initial-scale=1、width=device-width),直接传像素值。 + val density = LocalDensity.current + val insetTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val insetBottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val insetTopPx = with(density) { insetTop.roundToPx() } + val insetBottomPx = with(density) { insetBottom.roundToPx() } + + val dispatch: (String, Double?) -> Unit = remember( + onTogglePlay, + onSeek, + onClose, + onNext, + onPrev, + onToggleShuffle, + onCycleRepeat, + ) { + { command, value -> + when (command) { + "playPause" -> onTogglePlay() + "seek" -> value?.let { onSeek(it.toLong()) } + "close" -> onClose() + "next" -> onNext() + "prev" -> onPrev() + "shuffle" -> onToggleShuffle() + "repeat" -> onCycleRepeat() + } + } + } + DisposableEffect(dispatch) { + LyricPageState.onCommand.value = dispatch + onDispose { + if (LyricPageState.onCommand.value === dispatch) { + LyricPageState.onCommand.value = null + } + } + } + + LaunchedEffect(webView, loaded, rawLyric, lyricFormat, lyricTranslation) { + if (loaded) { + evalJs(webView, setRawLyricScript(rawLyric, lyricFormat, lyricTranslation)) + } + } + LaunchedEffect(webView, loaded, positionMs) { + if (loaded) { + webView?.evaluateJavascript(setTimeScript(positionMs), null) + } + } + LaunchedEffect(webView, loaded, playing) { + if (loaded) { + evalJs(webView, "window.amllNative&&window.amllNative.setPlaying($playing)") + } + } + LaunchedEffect(webView, loaded, title, artist, durationMs) { + if (loaded) { + evalJs( + webView, + "window.amllNative&&window.amllNative.setMeta(" + + "${jsStringLiteral(title)},${jsStringLiteral(artist)},${durationMs.coerceAtLeast(0)})", + ) + } + } + LaunchedEffect(webView, loaded, coverUrl) { + if (loaded) { + evalJs(webView, "window.amllNative&&window.amllNative.setCover(${jsStringLiteral(coverUrl)})") + } + } + LaunchedEffect(webView, loaded, dark) { + if (loaded) { + evalJs(webView, "window.amllNative&&window.amllNative.setTheme($dark)") + } + } + LaunchedEffect(webView, loaded, insetTopPx, insetBottomPx) { + if (loaded) { + evalJs( + webView, + "window.amllNative&&window.amllNative.setInsets($insetTopPx,$insetBottomPx)", + ) + } + } + + Box(modifier = modifier) { + key(LyricPageState.restart.intValue) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + if (BuildConfig.DEBUG) { + // 真机上可开 chrome://inspect 直接看页面 + WebView.setWebContentsDebuggingEnabled(true) + } + LyricPageState.webView.value?.let { cached -> + // 重新进入歌词页:直接复用已加载好的页面 + (cached.parent as? ViewGroup)?.removeView(cached) + cached + } ?: createPage(context.applicationContext, mainHandler).also { page -> + LyricPageState.webView.value = page + page.loadUrl(ASSET_URL) + } + }, + ) + } + } +} + +/** 创建一个只有歌词页用得到的 WebView(只在首次进入时执行) */ +private fun createPage(context: Context, mainHandler: Handler): WebView { + val assetLoader = WebViewAssetLoader.Builder() + .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context)) + .build() + return WebView(context).apply { + val self = this + // 透明底,加载期间透出 Compose 的深色背景,避免白闪 + setBackgroundColor(android.graphics.Color.TRANSPARENT) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + // 让页面按设备宽度布局。不开这个的话 WebView 会按 980px 桌面宽度排版, + // AMLL 面向移动端的尺寸/媒体查询全部失效,底部播控等会显示异常。 + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + settings.displayZoomControls = false + settings.builtInZoomControls = false + // 页面资源随 APK 更新,不走 WebView 缓存: + // 否则升级后可能还在用旧版 index.html 引用的旧 bundle,看起来像改动没生效 + settings.cacheMode = WebSettings.LOAD_NO_CACHE + // 页面只从 assets 加载:禁掉本地文件访问与弹窗,缩小 JS 桥的可达面 + settings.allowFileAccess = false + settings.allowContentAccess = false + settings.javaScriptCanOpenWindowsAutomatically = false + // 页面上的操作回传(在 JS 线程回调,切回主线程再执行) + addJavascriptInterface( + object { + @JavascriptInterface + fun postMessage(json: String) { + val obj = runCatching { JSONObject(json) }.getOrNull() ?: return + val command = obj.optString("command") + val value = if (obj.has("value")) obj.optDouble("value") else null + mainHandler.post { + // 桥只服务资产页,外部页面即便混进来也调不动 + if (self.url?.startsWith(ASSET_ORIGIN) == true) { + LyricPageState.onCommand.value?.invoke(command, value) + } + } + } + }, + "amllHost", + ) + webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(message: ConsoleMessage): Boolean { + // JS 接口不可用时的兜底:页面会退化成 "[amll-host] 命令 参数" + val text = message.message() + if (text.startsWith("[amll-host]")) { + val parts = text.removePrefix("[amll-host]").trim().split(WHITESPACE) + mainHandler.post { + if (self.url?.startsWith(ASSET_ORIGIN) == true) { + LyricPageState.onCommand.value?.invoke( + parts.firstOrNull().orEmpty(), + parts.getOrNull(1)?.toDoubleOrNull(), + ) + } + } + } + return true + } + } + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest?, + ): Boolean { + // 只放行资产页内部的跳转,外部链接一律拦掉 + return request?.url?.toString()?.startsWith(ASSET_ORIGIN) != true + } + + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest?, + ): WebResourceResponse? { + val url = request?.url ?: return null + return assetLoader.shouldInterceptRequest(url) + } + + override fun onPageFinished(view: WebView?, url: String?) { + // 数据不在这里推:composition 里按 loaded 触发的 effect 会补推当前值, + // 这样复用一个已加载的页面时也能立刻同步上。 + LyricPageState.loaded.value = true + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + if (request?.isForMainFrame == true) { + Log.w(TAG, "load failed: ${request.url} ${error?.errorCode} ${error?.description}") + } + } + + /** + * 渲染进程崩溃(OOM / 沙箱异常)时 WebViewClient 默认返回 false, + * 系统会连同宿主 App 一起杀掉。这里返回 true 表示已自行处理, + * 随后销毁坏实例并重建一个,歌词页只降级为一次重新加载。 + */ + override fun onRenderProcessGone( + view: WebView?, + detail: RenderProcessGoneDetail?, + ): Boolean { + Log.w(TAG, "render process gone, didCrash=${detail?.didCrash()}") + LyricPageState.loaded.value = false + LyricPageState.webView.value = null + view?.let { dead -> + (dead.parent as? ViewGroup)?.removeView(dead) + dead.destroy() + } + LyricPageState.restart.intValue++ + return true + } + } + } +} + +private val WHITESPACE = Regex("\\s+") + +private const val ASSET_ORIGIN = "https://appassets.androidplatform.net/assets/lyrics/" + +private const val ASSET_URL = ASSET_ORIGIN + "index.html" + +private fun setRawLyricScript(raw: String, format: String, translation: String) = + "window.amllNative&&window.amllNative.setRawLyric(" + + "${jsStringLiteral(raw)},${jsStringLiteral(format)},${jsStringLiteral(translation)})" + +private fun setTimeScript(positionMs: Long) = + "window.amllNative&&window.amllNative.setTime($positionMs)" + +private fun evalJs(webView: WebView?, script: String) { + webView?.evaluateJavascript(script, null) +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/AppBar.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/AppBar.kt new file mode 100644 index 0000000..d53b3b9 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/AppBar.kt @@ -0,0 +1,79 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronLeft +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Size + +/** 页面顶部栏 */ +@Composable +fun AmllTopBar( + title: String, + modifier: Modifier = Modifier, + onBack: (() -> Unit)? = null, + titleStyle: androidx.compose.ui.text.TextStyle = Font.navTitle, + trailing: @Composable RowScope.() -> Unit = {}, +) { + Row( + modifier = modifier.fillMaxWidth().height(Size.topBar), + verticalAlignment = Alignment.CenterVertically, + ) { + if (onBack != null) { + Icon( + imageVector = Icons.Rounded.ChevronLeft, + contentDescription = "返回", + tint = Amll.colors.ink, + modifier = Modifier + .size(18.dp) + .clickable(onClick = onBack), + ) + Spacer(Modifier.width(12.dp)) + } + Text( + text = title, + style = titleStyle, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + trailing() + } +} + +/** 大标题页头:24sp Bold + 可选副标题 */ +@Composable +fun PageHeader( + title: String, + subtitle: String? = null, + modifier: Modifier = Modifier, + trailing: @Composable RowScope.() -> Unit = {}, +) { + Column(modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(title, style = Font.pageTitle, color = Amll.colors.ink, modifier = Modifier.weight(1f)) + trailing() + } + if (subtitle != null) { + Spacer(Modifier.height(6.dp)) + Text(subtitle, style = Font.bodySmall, color = Amll.colors.ink3) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/Buttons.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/Buttons.kt new file mode 100644 index 0000000..8d5b2fc --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/Buttons.kt @@ -0,0 +1,101 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius + +/** 列表行尾的圆形播放按钮,纯装饰,点击由整行处理 */ +@Composable +fun PlayCircleButton( + modifier: Modifier = Modifier, + size: Dp = 30.dp, +) { + Box( + modifier + .size(size) + .clip(CircleShape) + .background(Amll.colors.primarySoft), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = null, + tint = Amll.colors.primary, + // PlayArrow 字形自带约 1/4 空白,按比例放大才和圆匹配 + modifier = Modifier.size(size * 0.62f).offset(x = 0.5.dp), + ) + } +} + +@Composable +fun PrimaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth().height(50.dp), + enabled = enabled, + shape = RoundedCornerShape(Radius.chip), + colors = ButtonDefaults.buttonColors( + containerColor = Amll.colors.primary, + contentColor = Amll.colors.onPrimary, + disabledContainerColor = Amll.colors.surface2, + disabledContentColor = Amll.colors.ink3, + ), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + Text( + text, + style = Font.body.copy(fontWeight = FontWeight.SemiBold, fontSize = 14.sp), + ) + } +} + +@Composable +fun GhostButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedButton( + onClick = onClick, + modifier = modifier.fillMaxWidth().height(50.dp), + shape = RoundedCornerShape(Radius.chip), + border = BorderStroke(1.dp, Amll.colors.border), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Amll.colors.surface, + contentColor = Amll.colors.ink, + ), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + Text(text, style = Font.body.copy(fontWeight = FontWeight.Medium)) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/CaptchaDialog.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/CaptchaDialog.kt new file mode 100644 index 0000000..019b2ce --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/CaptchaDialog.kt @@ -0,0 +1,167 @@ +package dev.amll.hub.android.ui.component + +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.DialogProperties +import dev.amll.hub.android.BuildConfig +import dev.amll.hub.android.data.remote.CaptchaConfig +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import android.graphics.Color as AndroidColor + +/** + * 人机验证弹层。 + * + * 阿里云验证码只接过网页端,这里用 WebView 加载 assets/captcha 里的桥接页, + * 页面内跑官方 SDK,验证通过后把 verifyParam 回传原生,再与网页端一样带 token 发码。 + * + * 注意:阿里云场景要求在控制台配置域名白名单,所以页面用站点域名作为 baseUrl 加载, + * 而不是 appassets 的本地域,避免「非法域名」。 + */ +@Composable +fun CaptchaDialog( + config: CaptchaConfig, + verified: Boolean?, + sending: Boolean, + error: String?, + onToken: (String) -> Unit, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val html = remember { + runCatching { + context.assets.open("captcha/index.html").bufferedReader().use { it.readText() } + }.getOrElse { "" } + } + var webView by remember { mutableStateOf(null) } + var loaded by remember { mutableStateOf(false) } + + LaunchedEffect(webView, loaded, verified) { + if (loaded && verified != null) { + webView?.evaluateJavascript("window.setCaptchaResult&&window.setCaptchaResult($verified)", null) + } + } + + AlertDialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + shape = RoundedCornerShape(Radius.sheet), + containerColor = Amll.colors.surface, + modifier = Modifier.padding(horizontal = 12.dp), + title = { Text("安全验证", style = Font.sheetTitle, color = Amll.colors.ink) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + "完成滑块验证后会自动发送验证码", + style = Font.micro, + color = Amll.colors.ink3, + ) + + Box( + Modifier + .fillMaxWidth() + // 阿里云 embed 模式拖动时会弹出拼图面板,高度留够才不会截断 + .height(360.dp), + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + WebView(ctx).apply { + setBackgroundColor(AndroidColor.TRANSPARENT) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.useWideViewPort = false + settings.loadWithOverviewMode = false + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + loaded = true + // 页面脚本就绪后再下发场景配置,避免依赖 query 参数 + view?.evaluateJavascript( + "window.startCaptcha&&window.startCaptcha(" + + "${jsStringLiteral(config.sceneId)},${jsStringLiteral(config.prefix)})", + null, + ) + } + } + addJavascriptInterface( + object { + @JavascriptInterface + fun onVerify(param: String) { + onToken(param) + } + }, + "AmllCaptcha", + ) + if (html.isBlank()) { + loaded = true + } else { + loadDataWithBaseURL(siteOrigin, html, "text/html", "utf-8", null) + } + webView = this + } + }, + ) + if (!loaded) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center).size(24.dp), + strokeWidth = 2.dp, + color = Amll.colors.primary, + ) + } + } + + if (sending) { + Text("验证已通过,正在发送验证码…", style = Font.micro, color = Amll.colors.ink3) + } else if (error != null) { + Text(error, style = Font.micro, color = Amll.colors.warning) + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("关闭", style = Font.caption, color = Amll.colors.ink3) + } + }, + ) +} + +/** 站点根域:https://api.beta.example.com → https://beta.example.com/ */ +private val siteOrigin: String + get() { + val base = BuildConfig.API_BASE_URL.trimEnd('/') + // 先保住原始 scheme,避免把 http 调试地址误升成 https + val scheme = when { + base.startsWith("https://") -> "https://" + base.startsWith("http://") -> "http://" + else -> "https://" + } + val host = base.removePrefix("https://").removePrefix("http://") + // api. 前缀是 Api 域名,验证码要求配置的是站点域名 + return scheme + host.removePrefix("api.") + "/" + } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/Cards.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/Cards.kt new file mode 100644 index 0000000..7e7f562 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/Cards.kt @@ -0,0 +1,238 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.CardShape +import dev.amll.hub.android.ui.theme.ChipShape +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import dev.amll.hub.android.ui.theme.Size +import dev.amll.hub.android.ui.theme.Space + +/** 白底描边卡片 */ +@Composable +fun AmllCard( + modifier: Modifier = Modifier, + padding: Dp = Space.xxl, + content: @Composable ColumnScope.() -> Unit, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = CardShape, + colors = CardDefaults.cardColors(containerColor = Amll.colors.surface), + border = BorderStroke(1.dp, Amll.colors.border), + ) { + Column(Modifier.padding(padding), content = content) + } +} + +@Composable +fun SectionHeader( + title: String, + modifier: Modifier = Modifier, + action: String? = null, + onAction: (() -> Unit)? = null, +) { + Row( + modifier.fillMaxWidth().height(22.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(title, style = Font.section.copy(fontSize = 14.sp), color = Amll.colors.ink) + if (action != null) { + Text( + action, + style = Font.caption, + color = Amll.colors.ink3, + modifier = Modifier.clickable(enabled = onAction != null) { onAction?.invoke() }, + ) + } + } +} + +/** 封面:有 URL 走 Coil;无 URL / 加载中 / 失败统一灰色底,与网页端一致 */ +@Composable +fun CoverImage( + url: String?, + seed: String, + size: Dp, + modifier: Modifier = Modifier, + radius: Dp = Radius.field, +) { + val shape = RoundedCornerShape(radius) + Box(modifier.size(size).clip(shape).background(Amll.colors.surface2)) { + if (!url.isNullOrBlank()) { + AsyncImage( + model = url, + contentDescription = seed, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } +} + +enum class ChipTone { PRIMARY, SUCCESS, WARNING, INFO, NEUTRAL } + +@Composable +fun StatusChip(text: String, tone: ChipTone = ChipTone.NEUTRAL, modifier: Modifier = Modifier) { + val (fg, bg) = when (tone) { + ChipTone.PRIMARY -> Amll.colors.primary to Amll.colors.primarySoft + ChipTone.SUCCESS -> Amll.colors.success to Amll.colors.successSoft + ChipTone.WARNING -> Amll.colors.warning to Amll.colors.warningSoft + ChipTone.INFO -> Amll.colors.info to Amll.colors.infoSoft + ChipTone.NEUTRAL -> Amll.colors.ink3 to Amll.colors.surface2 + } + AssistChip( + onClick = {}, + modifier = modifier, + label = { Text(text, style = Font.micro.copy(fontWeight = FontWeight.Medium)) }, + shape = ChipShape, + colors = AssistChipDefaults.assistChipColors(containerColor = bg, labelColor = fg), + border = null, + ) +} + +/** 通用列表行:封面 + 两行文字 + 尾部 */ +@Composable +fun SongRow( + title: String, + subtitle: String, + modifier: Modifier = Modifier, + coverUrl: String? = null, + coverSize: Dp = Size.coverRow, + leading: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + onClick: (() -> Unit)? = null, +) { + Row( + modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + // 上下 4dp:封面本身 48dp 已经把行撑开,再留 8dp 会显得松散 + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Space.l), + ) { + leading?.invoke() + CoverImage(coverUrl, title, coverSize) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text(title, style = Font.bodyMedium, color = Amll.colors.ink, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(subtitle, style = Font.caption, color = Amll.colors.ink3, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + // 右侧统一收一个播放按钮,行尾不会空掉(曲目接口没有时长字段) + if (trailing != null) { + trailing.invoke() + } else if (onClick != null) { + PlayCircleButton() + } + } +} + +/** 空态:图标 + 标题 + 说明 + 可选按钮 */ +@Composable +fun EmptyState( + icon: ImageVector, + title: String, + description: String, + modifier: Modifier = Modifier, + actionText: String? = null, + onAction: (() -> Unit)? = null, +) { + Column( + modifier.fillMaxWidth().padding(vertical = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + Modifier + .size(64.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Amll.colors.surface2), + contentAlignment = Alignment.Center, + ) { + Icon(icon, null, tint = Amll.colors.ink3, modifier = Modifier.size(28.dp)) + } + Text(title, style = Font.section.copy(fontSize = 15.sp), color = Amll.colors.ink) + Text(description, style = Font.bodySmall, color = Amll.colors.ink3) + if (actionText != null && onAction != null) { + Box( + Modifier + .height(38.dp) + .clip(RoundedCornerShape(19.dp)) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, RoundedCornerShape(19.dp)) + .clickable(onClick = onAction) + .padding(horizontal = 18.dp), + contentAlignment = Alignment.Center, + ) { + Text(actionText, style = Font.caption.copy(fontWeight = FontWeight.Medium), color = Amll.colors.ink) + } + } + } +} + +/** 卡片内的一行:标题 + 右侧箭头 */ +@Composable +fun NavRow( + label: String, + modifier: Modifier = Modifier, + value: String? = null, + onClick: (() -> Unit)? = null, +) { + Row( + modifier + .fillMaxWidth() + .height(46.dp) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(label, style = Font.body, color = Amll.colors.ink, modifier = Modifier.weight(1f)) + if (value != null) { + Text(value, style = Font.caption, color = Amll.colors.ink3) + Spacer(Modifier.width(6.dp)) + } + Icon( + Icons.Rounded.ChevronRight, + null, + tint = Amll.colors.ink3, + modifier = Modifier.size(16.dp), + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/ErrorBanner.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/ErrorBanner.kt index 44f4d15..f27f06c 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/component/ErrorBanner.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/ErrorBanner.kt @@ -1,41 +1,46 @@ package dev.amll.hub.android.ui.component +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius -/** 错误提示条,复用 Material3 的 Surface 做轻量卡片 */ +/** 错误提示条:与 App 的圆角/配色保持一致 */ @Composable fun ErrorBanner( message: String, modifier: Modifier = Modifier, ) { - Surface( - color = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - shape = MaterialTheme.shapes.small, - modifier = modifier.fillMaxWidth(), + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.field)) + .background(Amll.colors.primarySoft) + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - ) { - Icon( - imageVector = Icons.Filled.ErrorOutline, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp), - ) - Text(text = message, style = MaterialTheme.typography.bodyMedium) - } + Icon( + imageVector = Icons.Filled.ErrorOutline, + contentDescription = null, + tint = Amll.colors.primary, + modifier = Modifier.size(16.dp), + ) + Text(text = message, style = Font.caption, color = Amll.colors.primary) } } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/Inputs.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/Inputs.kt new file mode 100644 index 0000000..957a461 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/Inputs.kt @@ -0,0 +1,205 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius + +/** 分段切换:登录方式、验证码载体这类二选一 */ +@Composable +fun SegmentedTabs( + labels: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + SingleChoiceSegmentedButtonRow(modifier.fillMaxWidth()) { + labels.forEachIndexed { index, label -> + val active = index == selectedIndex + SegmentedButton( + selected = active, + onClick = { onSelect(index) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = labels.size), + colors = SegmentedButtonDefaults.colors( + activeContainerColor = Amll.colors.surface, + activeContentColor = Amll.colors.ink, + activeBorderColor = Amll.colors.border, + inactiveContainerColor = Amll.colors.surface2, + inactiveContentColor = Amll.colors.ink3, + inactiveBorderColor = Color.Transparent, + ), + // 不加 M3 默认的对勾:这里的状态只用底色区分 + icon = {}, + label = { + Text( + label, + style = Font.caption.copy( + fontWeight = if (active) FontWeight.SemiBold else FontWeight.Normal, + ), + ) + }, + ) + } + } +} + +/** 圆角输入框:登录页与表单统一用这套,底层是 M3 OutlinedTextField */ +@Composable +fun AmllTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + label: String? = null, + keyboardType: KeyboardType = KeyboardType.Text, + imeAction: ImeAction = ImeAction.Next, + visualTransformation: VisualTransformation = VisualTransformation.None, + onImeAction: (() -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, +) { + Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) { + if (label != null) { + Text(label, style = Font.micro, color = Amll.colors.ink3) + } + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + textStyle = Font.body, + placeholder = { + Text(placeholder, style = Font.bodySmall, color = Amll.colors.ink3) + }, + singleLine = true, + shape = RoundedCornerShape(Radius.chip), + visualTransformation = visualTransformation, + trailingIcon = trailing, + keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction), + keyboardActions = KeyboardActions(onAny = { onImeAction?.invoke() }), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Amll.colors.surface2, + unfocusedContainerColor = Amll.colors.surface2, + focusedTextColor = Amll.colors.ink, + unfocusedTextColor = Amll.colors.ink, + cursorColor = Amll.colors.primary, + focusedBorderColor = Amll.colors.primary, + unfocusedBorderColor = Color.Transparent, + focusedPlaceholderColor = Amll.colors.ink3, + unfocusedPlaceholderColor = Amll.colors.ink3, + ), + ) + } +} + +/** 胶囊搜索框:搜索页专用样式,图标 + 占位符 + 清除按钮 */ +@Composable +fun SearchPillField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "搜索", + focusRequester: FocusRequester? = null, + onSearch: (() -> Unit)? = null, + onClear: () -> Unit = {}, +) { + Row( + modifier + .height(42.dp) + .clip(RoundedCornerShape(Radius.chip)) + .background(Amll.colors.surface2) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Rounded.Search, null, tint = Amll.colors.ink3, modifier = Modifier.size(16.dp)) + Spacer(Modifier.width(8.dp)) + Box(Modifier.weight(1f)) { + if (value.isEmpty()) { + Text(placeholder, style = Font.bodySmall, color = Amll.colors.ink3) + } + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + textStyle = TextStyle(fontSize = 13.5.sp, color = Amll.colors.ink), + cursorBrush = SolidColor(Amll.colors.primary), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearch?.invoke() }), + modifier = Modifier + .fillMaxWidth() + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier), + ) + } + if (value.isNotEmpty()) { + Icon( + Icons.Rounded.Close, + "清空", + tint = Amll.colors.ink3, + modifier = Modifier.size(16.dp).clickable(onClick = onClear), + ) + } + } +} + +/** 表单行:左侧标签 + 右侧值 */ +@Composable +fun FieldRow( + label: String, + value: String, + modifier: Modifier = Modifier, + valueColor: Color? = null, + onClick: (() -> Unit)? = null, +) { + Row( + modifier + .fillMaxWidth() + .height(44.dp) + .clip(RoundedCornerShape(Radius.field)) + .background(Amll.colors.surface2) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(label, style = Font.bodySmall, color = Amll.colors.ink3) + Text(value, style = Font.bodyMedium, color = valueColor ?: Amll.colors.ink, maxLines = 1) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/JsLiteral.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/JsLiteral.kt new file mode 100644 index 0000000..fdab051 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/JsLiteral.kt @@ -0,0 +1,12 @@ +package dev.amll.hub.android.ui.component + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private val json = Json + +/** + * 把任意字符串编码成合法的 JS/JSON 字面量(含引号)。 + * 换行、控制字符、Unicode 分隔符都由 kotlinx.serialization 统一处理。 + */ +internal fun jsStringLiteral(value: String): String = json.encodeToString(value) diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/MarkupText.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/MarkupText.kt new file mode 100644 index 0000000..cefe884 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/MarkupText.kt @@ -0,0 +1,100 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font + +/** + * 推荐语标记语言渲染(与网页端 markup.ts 的核心规则一致): + * `[:-:]` 居中、`[-:]` 右对齐、`[_:]` 小字,`[-]` 分割线。 + */ +private data class MarkupLine( + val text: String, + val align: TextAlign, + val small: Boolean, + val separator: Boolean, +) + +private val MARKUP_REGEX = Regex("^\\[([:_-]+)\\](.*)$") + +private fun parseMarkupLines(source: String): List { + if (source.isBlank()) return emptyList() + val result = ArrayList() + source.split('\n').forEach { rawLine -> + val line = rawLine.trim() + val match = MARKUP_REGEX.find(line) + if (match == null) { + result += MarkupLine(line, TextAlign.Start, small = false, separator = false) + return@forEach + } + val spec = match.groupValues[1] + val content = match.groupValues[2].trim() + if (spec == "-") { + if (content.isNotEmpty()) result += MarkupLine(content, TextAlign.Center, false, true) + return@forEach + } + val normalized = spec.replace('_', '-') + val align = when { + normalized.contains(":-:") -> TextAlign.Center + normalized.contains("-:") -> TextAlign.End + else -> TextAlign.Start + } + result += MarkupLine(content, align, small = spec.contains('_'), separator = false) + } + return result +} + +@Composable +fun MarkupText( + text: String, + modifier: Modifier = Modifier, + color: Color = Amll.colors.ink2, + emptyText: String = "今天没有留下评论", +) { + val lines = remember(text) { parseMarkupLines(text) } + Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) { + if (lines.isEmpty()) { + Text(emptyText, style = Font.bodySmall, color = Amll.colors.ink3) + return@Column + } + lines.forEach { line -> + if (line.separator) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box(Modifier.weight(1f).height(1.dp).background(Amll.colors.border)) + Text(line.text, style = Font.micro, color = Amll.colors.ink3) + Box(Modifier.weight(1f).height(1.dp).background(Amll.colors.border)) + } + } else { + Text( + line.text, + style = Font.bodySmall.copy( + fontSize = if (line.small) 12.sp else 15.sp, + lineHeight = if (line.small) 18.sp else 23.sp, + ), + color = color, + textAlign = line.align, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/MiniPlayer.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/MiniPlayer.kt new file mode 100644 index 0000000..7c99830 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/MiniPlayer.kt @@ -0,0 +1,188 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Pause +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.QueueMusic +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.playback.PlayerController +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius + +/** + * 迷你播放器:贴在底部 Tab 栏上方,点封面或信息区进 AMLL 歌词页。 + * 没有曲目时整块不占位。 + * + * 卡片本身不铺底色,毛玻璃由外层传进来的 [modifier] 提供, + * 这样下面滚动的内容才能透上来。 + */ +@Composable +fun MiniPlayer( + state: PlayerController.State, + /** 正在解析歌曲信息(openTrack 进行中) */ + loading: Boolean, + /** 播放器层面的错误,如「该歌曲暂无音源」 */ + error: String?, + /** 当前正在播放的歌词行,没有时第二行回落到歌手 */ + lyricLine: String?, + onOpen: () -> Unit, + onToggle: () -> Unit, + onTogglePlaylist: () -> Unit, + modifier: Modifier = Modifier, +) { + val track = state.track ?: return + val fraction = if (state.durationMs > 0) { + (state.positionMs.toFloat() / state.durationMs.toFloat()).coerceIn(0f, 1f) + } else { + 0f + } + val shape = RoundedCornerShape(Radius.card) + + Column( + Modifier + .fillMaxWidth() + // 与胶囊导航左右对齐(都是 12dp) + .padding(horizontal = 12.dp) + .fillMaxWidth() + // 投影让播放条在「后面没内容」时也能和页面区分开 + .shadow(elevation = 8.dp, shape = shape, clip = false) + // 先裁圆角再叠毛玻璃,否则模糊会画到圆角外面 + .clip(shape) + // haze 跳过绘制时的实心底兜底,避免播放条透明 + .background(Amll.colors.surface.copy(alpha = 0.97f)) + .then(modifier) + .border(0.5.dp, Amll.colors.border.copy(alpha = 0.55f), shape) + .clickable(onClick = onOpen), + ) { + Row( + Modifier + .fillMaxWidth() + .height(54.dp) + .padding(start = 9.dp, end = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + // 封面单独可点:与网页端一致,点封面直接进歌词页 + Box(Modifier.clip(RoundedCornerShape(10.dp)).clickable(onClick = onOpen)) { + CoverImage(track.coverUrl, track.title, 38.dp, radius = 10.dp) + } + Column( + Modifier.weight(1f).clickable(onClick = onOpen), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + track.title.ifBlank { "正在播放" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + when { + loading -> "正在获取歌曲信息…" + !error.isNullOrBlank() -> error + !lyricLine.isNullOrBlank() -> lyricLine + else -> track.artist.ifBlank { "AMLL Hub" } + }, + style = Font.micro, + color = if (!error.isNullOrBlank()) Amll.colors.warning else Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .size(36.dp) + .clip(CircleShape) + .background(Amll.colors.surface2.copy(alpha = 0.7f)) + .clickable(enabled = !loading, onClick = onToggle), + contentAlignment = Alignment.Center, + ) { + if (loading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = Amll.colors.primary, + ) + } else { + Icon( + imageVector = if (state.playing) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = if (state.playing) "暂停" else "播放", + tint = Amll.colors.ink, + modifier = Modifier.size(20.dp), + ) + } + } + Box( + Modifier + .size(36.dp) + .clip(CircleShape) + .background(Amll.colors.surface2.copy(alpha = 0.7f)) + // 内层 clickable 会消费事件,不会落到外层卡片进歌词页 + .clickable(onClick = onTogglePlaylist), + contentAlignment = Alignment.Center, + ) { + BadgedBox( + badge = { + if (state.queue.isNotEmpty()) { + Badge( + containerColor = Amll.colors.primary, + contentColor = Amll.colors.onPrimary, + ) { + Text(state.queue.size.toString()) + } + } + }, + ) { + Icon( + imageVector = Icons.Rounded.QueueMusic, + contentDescription = "播放列表", + tint = Amll.colors.ink, + modifier = Modifier.size(20.dp), + ) + } + } + } + } + LinearProgressIndicator( + progress = { fraction }, + modifier = Modifier.fillMaxWidth().height(2.dp), + color = Amll.colors.primary, + trackColor = Amll.colors.ink3.copy(alpha = 0.22f), + strokeCap = StrokeCap.Butt, + gapSize = 0.dp, + drawStopIndicator = {}, + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/PlaylistSheet.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/PlaylistSheet.kt new file mode 100644 index 0000000..803b239 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/PlaylistSheet.kt @@ -0,0 +1,183 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.DeleteSweep +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.QueueMusic +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.playback.PlayerController +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import dev.amll.hub.android.ui.theme.SheetShape + +/** + * 播放列表底部弹层:展示当前队列,可切歌、移除、清空。 + * + * 队列数据与播放状态都来自 [PlayerController.State];点击条目只切歌并关闭弹层, + * 不导航离开当前页面。 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlaylistSheet( + state: PlayerController.State, + onDismiss: () -> Unit, + onPlayAt: (Int) -> Unit, + onRemove: (Int) -> Unit, + onClear: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val listState = rememberLazyListState() + val currentIndex = state.currentIndex + + // 打开时与切歌后都把当前曲目滚到可见位置(留两行上下文) + LaunchedEffect(currentIndex, state.queue.size) { + if (currentIndex in state.queue.indices) { + listState.animateScrollToItem((currentIndex - 2).coerceAtLeast(0)) + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = SheetShape, + containerColor = Amll.colors.surface, + dragHandle = { BottomSheetDefaults.DragHandle(color = Amll.colors.border) }, + ) { + Column(Modifier.fillMaxWidth().padding(bottom = 24.dp)) { + Row( + Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("播放列表", style = Font.sheetTitle, color = Amll.colors.ink) + Spacer(Modifier.size(8.dp)) + Text( + "${state.queue.size} 首", + style = Font.caption, + color = Amll.colors.ink3, + ) + Spacer(Modifier.weight(1f)) + IconButton(onClick = onClear, enabled = state.queue.isNotEmpty()) { + Icon( + imageVector = Icons.Rounded.DeleteSweep, + contentDescription = "清空播放列表", + tint = if (state.queue.isNotEmpty()) { + Amll.colors.ink3 + } else { + Amll.colors.ink3.copy(alpha = 0.35f) + }, + modifier = Modifier.size(20.dp), + ) + } + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = "关闭", + tint = Amll.colors.ink3, + modifier = Modifier.size(20.dp), + ) + } + } + + if (state.queue.isEmpty()) { + EmptyState( + icon = Icons.Rounded.QueueMusic, + title = "播放列表为空", + description = "在网易云解析页选一首歌,或点「播放全部」", + ) + } else { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxWidth().fillMaxHeight(0.62f), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + ) { + itemsIndexed(state.queue) { index, item -> + val selected = index == currentIndex + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.chip)) + .background(if (selected) Amll.colors.primarySoft else Color.Transparent) + .clickable { onPlayAt(index) } + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // 固定宽度占位,非当前曲目也保持封面左对齐 + Box(Modifier.size(16.dp), contentAlignment = Alignment.Center) { + if (selected) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = null, + tint = Amll.colors.primary, + modifier = Modifier.size(16.dp), + ) + } + } + CoverImage(item.coverUrl, item.name, 42.dp) + Column( + Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + item.name.ifBlank { "未知歌曲" }, + style = Font.bodyMedium, + color = if (selected) Amll.colors.primary else Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + item.artists.ifBlank { "未知歌手" }, + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + // IconButton 自己消费点击,不会触发行点击 + IconButton(onClick = { onRemove(index) }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = "从播放列表移除", + tint = Amll.colors.ink3, + modifier = Modifier.size(18.dp), + ) + } + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/QrScannerView.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/QrScannerView.kt index f013c78..9f4d343 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/component/QrScannerView.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/QrScannerView.kt @@ -11,7 +11,6 @@ import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView -import androidx.concurrent.futures.await import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,23 +30,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.concurrent.futures.await import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage -import java.util.concurrent.Executors +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor /** * 相机扫码预览:CameraX 提供预览、ML Kit 识别二维码。 * 只识别 QR_CODE;识别成功后停止分析,避免重复回调。 */ -@OptIn(ExperimentalGetImage::class) @Composable fun QrScannerView( onQrScanned: (String) -> Unit, @@ -59,11 +59,11 @@ fun QrScannerView( var hasPermission by remember { mutableStateOf( ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == - PackageManager.PERMISSION_GRANTED + PackageManager.PERMISSION_GRANTED, ) } val permissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() + ActivityResultContracts.RequestPermission(), ) { granted -> hasPermission = granted } LaunchedEffect(Unit) { @@ -78,7 +78,8 @@ fun QrScannerView( return } - val analysisExecutor = remember { Executors.newSingleThreadExecutor() } + // KEEP_ONLY_LATEST 已保证串行分析,不需要再独占一条线程 + val analysisExecutor = remember { Dispatchers.IO.asExecutor() } val latestCallback by rememberUpdatedState(onQrScanned) var cameraProvider by remember { mutableStateOf(null) } @@ -86,7 +87,7 @@ fun QrScannerView( BarcodeScanning.getClient( BarcodeScannerOptions.Builder() .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() + .build(), ) } @@ -123,9 +124,9 @@ fun QrScannerView( DisposableEffect(Unit) { onDispose { + // unbindAll 会清空 analyzer,扫描线程随之停止 cameraProvider?.unbindAll() scanner.close() - analysisExecutor.shutdown() } } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/QualityPicker.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/QualityPicker.kt new file mode 100644 index 0000000..97d2364 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/QualityPicker.kt @@ -0,0 +1,97 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.playback.NcmQuality +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.ChipShape +import dev.amll.hub.android.ui.theme.Font + +/** 网易云音质选择:胶囊按钮 + 下拉菜单,所有入口共用页面音质 */ +@Composable +fun QualityPicker( + selected: NcmQuality, + onSelect: (NcmQuality) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Box(modifier) { + Row( + Modifier + .height(28.dp) + .clip(ChipShape) + .background(Amll.colors.surface2) + .clickable { expanded = true } + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + "音质 · ${selected.label}", + style = Font.micro, + color = Amll.colors.ink2, + maxLines = 1, + ) + Icon( + Icons.Rounded.ExpandMore, + null, + tint = Amll.colors.ink3, + modifier = Modifier.size(14.dp), + ) + } + + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + NcmQuality.ORDER.forEach { quality -> + val active = quality == selected + DropdownMenuItem( + text = { + Text( + quality.label, + style = Font.caption, + color = if (active) Amll.colors.primary else Amll.colors.ink, + ) + }, + onClick = { + expanded = false + onSelect(quality) + }, + trailingIcon = if (active) { + { + Icon( + Icons.Rounded.Check, + null, + tint = Amll.colors.primary, + modifier = Modifier.size(16.dp), + ) + } + } else { + null + }, + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/Skeleton.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/Skeleton.kt new file mode 100644 index 0000000..ede6247 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/Skeleton.kt @@ -0,0 +1,43 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Radius +import dev.amll.hub.android.ui.theme.Space + +/** 骨架行 */ +@Composable +fun SkeletonRow(modifier: Modifier = Modifier) { + Row( + modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Space.l), + ) { + Box(Modifier.size(48.dp).clip(RoundedCornerShape(Radius.field)).background(Amll.colors.surface2)) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Box(Modifier.fillMaxWidth(0.62f).height(11.dp).clip(RoundedCornerShape(5.dp)).background(Amll.colors.surface2)) + Box(Modifier.fillMaxWidth(0.36f).height(9.dp).clip(RoundedCornerShape(4.dp)).background(Amll.colors.surface2)) + } + } +} + +@Composable +fun SkeletonList(count: Int = 4, modifier: Modifier = Modifier) { + Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(Space.s)) { + repeat(count) { SkeletonRow() } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/component/TabBar.kt b/android/app/src/main/java/dev/amll/hub/android/ui/component/TabBar.kt new file mode 100644 index 0000000..84eda80 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/component/TabBar.kt @@ -0,0 +1,184 @@ +package dev.amll.hub.android.ui.component + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.PillOuterShape +import dev.amll.hub.android.ui.theme.Size +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +data class TabItem(val label: String, val icon: ImageVector) + +/** + * 底部胶囊导航。 + * + * 选中块可以「拖着走」:按住左右拖,指示块跟手,松手吸附到最近的 Tab。 + */ +@Composable +fun PillTabBar( + items: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + val target = selectedIndex.coerceIn(0, (items.size - 1).coerceAtLeast(0)) + val scope = rememberCoroutineScope() + // 用 Animatable 而不是 animateFloatAsState:拖动结束时要先 snap 到手指位置, + // 再从那里接着弹到目标,animateFloatAsState 做不到中途接管 + val progress = remember { Animatable(target.toFloat()) } + val springSpec = spring(dampingRatio = 0.86f, stiffness = Spring.StiffnessMediumLow) + LaunchedEffect(target) { + progress.animateTo(target.toFloat(), springSpec) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + // 左右 12dp:每个 Tab 的槽位更宽,指示块才铺得开 + .padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 18.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(Size.tabPill) + // 投影让底栏在「后面没内容」时也能和页面区分开; + // 否则毛玻璃糊出来的是纯色,看起来就像没有这一层 + .shadow(elevation = 8.dp, shape = PillOuterShape, clip = false) + // 先裁圆角再叠毛玻璃,否则模糊会画到胶囊外面 + .clip(PillOuterShape) + // haze 在源内容静止/不可用时整个跳过绘制(连底色都不画), + // 这层实心底就是那时的兜底,避免底栏透明 + .background(Amll.colors.surface.copy(alpha = 0.97f)) + .then(modifier) + .border(0.5.dp, Amll.colors.border.copy(alpha = 0.55f), PillOuterShape) + .padding(4.dp), + ) { + BoxWithConstraints(Modifier.fillMaxSize()) { + val slot = maxWidth / items.size.toFloat() + val slotPx = with(LocalDensity.current) { slot.toPx() } + // 拖动量按像素累计,渲染时换算成槽位数叠加 + var dragPx by remember(slotPx) { mutableFloatStateOf(0f) } + val dragSlots = if (slotPx > 0f) dragPx / slotPx else 0f + val position = (progress.value + dragSlots) + .coerceIn(0f, (items.size - 1).toFloat()) + + // 选中态是「包住图标+文字」的扁圆角块,左右各留 4dp。 + // 圆角用 50%(= 高度一半),和外壳胶囊是同一档弧度,不会是「内方外圆」 + val indicatorWidth = slot - 8.dp + Box( + Modifier + .offset(x = slot * position + 4.dp) + .width(indicatorWidth) + .fillMaxHeight() + .clip(RoundedCornerShape(percent = 50)) + .background(Amll.colors.primarySoft), + ) + + Row( + Modifier + .fillMaxSize() + .pointerInput(items.size, slotPx) { + detectHorizontalDragGestures( + onDragStart = { dragPx = 0f }, + onDragCancel = { dragPx = 0f }, + onHorizontalDrag = { change, amount -> + change.consume() + dragPx += amount + }, + onDragEnd = { + val delta = if (slotPx > 0f) dragPx / slotPx else 0f + val landed = (target + delta) + .coerceIn(0f, (items.size - 1).toFloat()) + val next = landed.roundToInt() + dragPx = 0f + scope.launch { + // 先落到手指松开的位置,避免弹回原处再出发 + progress.snapTo(landed) + if (next != target) { + onSelect(next) + } else { + progress.animateTo(target.toFloat(), springSpec) + } + } + }, + ) + }, + ) { + items.forEachIndexed { index, item -> + val active = index == target + val tint by animateColorAsState( + targetValue = if (active) Amll.colors.primary else Amll.colors.ink3, + animationSpec = tween(180), + label = "tabTint$index", + ) + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + // 不要正方形水波纹:只响应点击,不画指示 + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onSelect(index) }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = item.icon, + contentDescription = item.label, + tint = tint, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.height(3.dp)) + Text(item.label, style = Font.tabLabel, color = tint) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorCenterScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorCenterScreen.kt new file mode 100644 index 0000000..73cc40d --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorCenterScreen.kt @@ -0,0 +1,342 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.SubmissionDto +import dev.amll.hub.android.data.remote.SubmissionStatsDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.TabScaffold +import dev.amll.hub.android.ui.bottomBarPadding +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.ChipTone +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.GhostButton +import dev.amll.hub.android.ui.component.PageHeader +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.component.SegmentedTabs +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.component.StatusChip +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.ChipShape +import dev.amll.hub.android.ui.theme.Font + +/* ============================ 07 创作中心 ============================ */ + +/** 歌词管理的状态筛选,取值与后端 repository 的 status 参数一致 */ +private val CREATOR_STATUS = listOf( + "all" to "全部", + "draft" to "草稿", + "processing" to "进行中", + "approved" to "已通过", + "rejected" to "未通过", +) + +private val CREATOR_TABS = listOf("歌词", "每日推荐", "搜索 IP") + +@Composable +fun CreatorCenterScreen(vm: CreatorViewModel, nav: AppNav) { + LaunchedEffect(Unit) { + vm.loadSubmissions() + vm.loadSubmissionStats() + } + val stats by vm.submissionStats.collectAsStateWithLifecycle() + val subUi by vm.submissions.collectAsStateWithLifecycle() + val dailyUi by vm.dailySubmissions.collectAsStateWithLifecycle() + val ipUi by vm.searchIpSubmissions.collectAsStateWithLifecycle() + val status by vm.submissionStatus.collectAsStateWithLifecycle() + var mainTab by remember { mutableIntStateOf(0) } + + TabScaffold { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 20.dp, + end = 20.dp, + top = 10.dp, + bottom = bottomBarPadding(16.dp), + ), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { PageHeader("创作中心", "投稿进度、修订确认都在这里") } + + item { + // 投稿入口:与网页端「投稿」菜单一致,支持歌词与每日推荐两类 + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + PrimaryButton( + text = "歌词投稿", + onClick = nav.openSubmitLyric, + modifier = Modifier.weight(1f), + ) + GhostButton( + text = "每日推荐投稿", + onClick = nav.openSubmitDaily, + modifier = Modifier.weight(1f), + ) + } + } + + item { SubmissionStatsCard(stats) } + + item { + SectionHeader("我的投稿") + } + + item { + SegmentedTabs( + labels = CREATOR_TABS, + selectedIndex = mainTab, + onSelect = { index -> + mainTab = index + when (index) { + 0 -> vm.loadSubmissions() + 1 -> vm.loadDailySubmissions() + else -> vm.loadSearchIpSubmissions() + } + }, + ) + } + + when (mainTab) { + 0 -> { + item { + Row( + Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + CREATOR_STATUS.forEach { (value, label) -> + FilterChip( + selected = value == status, + onClick = { vm.loadSubmissions(value) }, + label = { + Text( + label, + style = Font.micro.copy(fontWeight = FontWeight.Medium), + ) + }, + shape = ChipShape, + colors = FilterChipDefaults.filterChipColors( + containerColor = Amll.colors.surface2, + labelColor = Amll.colors.ink2, + selectedContainerColor = Amll.colors.primary, + selectedLabelColor = Amll.colors.onPrimary, + ), + border = null, + ) + } + } + } + when { + subUi.loading -> item { SkeletonList(4) } + subUi.items.isEmpty() -> item { + EmptyState( + icon = Icons.Outlined.Inbox, + title = subUi.error ?: "暂无稿件", + description = "换个筛选条件试试", + ) + } + else -> items(subUi.items, key = { it.id }) { item -> + SubmissionRow(item) { id -> + nav.openSubmission(id) + } + } + } + } + + 1 -> when { + dailyUi.loading -> item { SkeletonList(4) } + dailyUi.items.isEmpty() -> item { + EmptyState( + icon = Icons.Outlined.Inbox, + title = dailyUi.error ?: "暂无每日推荐投稿", + description = "去投一首今天想推荐给大家的歌", + ) + } + else -> items(dailyUi.items, key = { it.id }) { item -> + DailySubmissionRow(item) + } + } + + else -> when { + ipUi.loading -> item { SkeletonList(4) } + ipUi.items.isEmpty() -> item { + EmptyState( + icon = Icons.Outlined.Inbox, + title = ipUi.error ?: "暂无搜索 IP 投稿", + description = "搜索 IP 显示投稿会出现在这里", + ) + } + else -> items(ipUi.items, key = { it.id }) { item -> + Row( + Modifier.fillMaxWidth().height(56.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + item.title.ifBlank { "未命名投稿" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + "#${item.id} · ${item.createdAt.take(10)}", + style = Font.micro, + color = Amll.colors.ink3, + ) + } + val (label, tone) = statusOf(item.status) + StatusChip(label, tone) + } + } + } + } + + item { + AmllCard(padding = 14.dp) { + Text( + "逐字打轴请在电脑端完成,手机端可以查看进度、确认修订版", + style = Font.micro, + color = Amll.colors.ink3, + ) + } + } + } + } +} + +/** 统计卡:数字全部来自 /submissions/stats */ +@Composable +private fun SubmissionStatsCard(stats: SubmissionStatsDto?) { + val cells = listOf( + "进行中" to (stats?.processing ?: 0), + "已通过" to (stats?.approved ?: 0), + "待确认" to (stats?.revised ?: 0), + "未通过" to (stats?.rejected ?: 0), + ) + AmllCard(padding = 14.dp) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + cells.forEach { (label, value) -> + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + value.toString(), + style = Font.numberL.copy(fontSize = 22.sp), + color = if (value > 0) Amll.colors.ink else Amll.colors.ink3, + ) + Text(label, style = Font.micro, color = Amll.colors.ink3) + } + } + } + } +} + +@Composable +private fun DailySubmissionRow(item: DailyRecDto) { + Row( + Modifier.fillMaxWidth().height(56.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + item.songName.ifBlank { "未命名推荐" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + "${item.date} · ${item.artist.ifBlank { "未知歌手" }}", + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + ) + } + val (label, tone) = statusOf(item.status) + StatusChip(label, tone) + } +} + +@Composable +private fun SubmissionRow(item: SubmissionDto, onOpen: (Long) -> Unit) { + Row( + Modifier.fillMaxWidth().height(60.dp).clickable { onOpen(item.id) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + item.displayName, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + "#${item.id} · ${item.createdAt.take(10)} · ${languageText(item.language)}", + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + ) + } + val (label, tone) = statusOf(item.status) + StatusChip(label, tone) + } +} + +/** 语言代码 → 中文名,未收录的直接显示原值 */ +internal fun languageText(code: String): String { + if (code.isBlank()) return "未标注" + return when (code) { + "zh" -> "汉语" + "en" -> "英语" + "ja" -> "日语" + "ko" -> "韩语" + "others" -> "其他" + else -> code + } +} + +internal fun statusOf(status: String): Pair = when (status) { + "approved" -> "已通过" to ChipTone.SUCCESS + "pending" -> "待审核" to ChipTone.WARNING + "reviewing" -> "审核中" to ChipTone.INFO + "need_revision" -> "需修改" to ChipTone.WARNING + "missing_audio" -> "缺音频" to ChipTone.WARNING + "revised" -> "待确认修订" to ChipTone.PRIMARY + "rejected" -> "未通过" to ChipTone.PRIMARY + "draft" -> "草稿" to ChipTone.NEUTRAL + "closed" -> "已关闭" to ChipTone.NEUTRAL + else -> status.ifBlank { "未知" } to ChipTone.NEUTRAL +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorViewModel.kt new file mode 100644 index 0000000..a63bd26 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/CreatorViewModel.kt @@ -0,0 +1,126 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.SearchIpSubmissionDto +import dev.amll.hub.android.data.remote.SubmissionDto +import dev.amll.hub.android.data.remote.SubmissionStatsDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** 创作中心:歌词管理 + 每日推荐投稿 + 搜索 IP 投稿 + 稿件详情 */ +@HiltViewModel +class CreatorViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val submissions = MutableStateFlow(ListUi()) + val submissionStatus = MutableStateFlow("all") + val submissionStats = MutableStateFlow(null) + val dailySubmissions = MutableStateFlow(ListUi()) + val searchIpSubmissions = MutableStateFlow(ListUi()) + val submissionDetail = MutableStateFlow(null) + + /** 歌词管理:列表 + 顶部统计,状态筛选与网页端一致 */ + fun loadSubmissions(status: String = submissionStatus.value) { + submissionStatus.value = status + viewModelScope.launch { + submissions.update { it.copy(loading = true, error = null) } + val result = repo.submissions(status) + val items = result.getOrDefault(emptyList()) + submissions.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无稿件" else null, + ) + } + } + } + + fun loadSubmissionStats() { + viewModelScope.launch { submissionStats.value = repo.submissionStats().getOrNull() } + } + + fun loadDailySubmissions() { + viewModelScope.launch { + dailySubmissions.update { it.copy(loading = true, error = null) } + val result = repo.dailySubmissions() + val items = result.getOrDefault(emptyList()) + dailySubmissions.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无每日推荐投稿" else null, + ) + } + } + } + + fun loadSearchIpSubmissions() { + viewModelScope.launch { + searchIpSubmissions.update { it.copy(loading = true, error = null) } + val result = repo.searchIpSubmissions() + val items = result.getOrDefault(emptyList()) + searchIpSubmissions.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无搜索 IP 投稿" else null, + ) + } + } + } + + /** 稿件详情:按路由参数里的 id 拉取 */ + fun loadSubmission(id: Long) { + if (id <= 0L) return + submissionDetail.value = submissionDetail.value?.takeIf { it.id == id } + viewModelScope.launch { + repo.submission(id).getOrNull()?.let { submissionDetail.value = it } + } + } + + /** 稿件操作:成功后就地刷新列表与统计 */ + fun closeSubmission(id: Long, onDone: (Boolean) -> Unit = {}) { + viewModelScope.launch { + val ok = repo.closeSubmission(id).getOrDefault(false) + if (ok) refreshCreator() + onDone(ok) + } + } + + fun adoptRevision(id: Long, onDone: (Boolean) -> Unit = {}) { + viewModelScope.launch { + val ok = repo.adoptRevision(id).getOrDefault(false) + if (ok) refreshCreator() + onDone(ok) + } + } + + fun rejectRevision(id: Long, onDone: (Boolean) -> Unit = {}) { + viewModelScope.launch { + val ok = repo.rejectRevision(id).getOrDefault(false) + if (ok) refreshCreator() + onDone(ok) + } + } + + private suspend fun refreshCreator() { + val items = repo.submissions(submissionStatus.value).getOrDefault(emptyList()) + submissions.update { it.copy(items = items, loading = false) } + submissionStats.value = repo.submissionStats().getOrNull() + // 详情页标题栏的状态也要跟着变 + submissionDetail.value = submissionDetail.value?.let { d -> + items.firstOrNull { it.id == d.id } ?: d + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyScreen.kt new file mode 100644 index 0000000..ae9ef70 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyScreen.kt @@ -0,0 +1,522 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.rounded.ChevronLeft +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.Urls +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.MarkupText +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.TextStyle +import java.util.Locale + +/* ============================ 04 每日推荐 ============================ */ + +private val WEEKDAYS = listOf("日", "一", "二", "三", "四", "五", "六") + +@Composable +fun DailyScreen(vm: DailyViewModel, nav: AppNav) { + val ui by vm.daily.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { if (vm.daily.value.items.isEmpty()) vm.loadDaily() } + + val today = remember { LocalDate.now() } + val todayKey = today.toString() + var selectedDate by remember { mutableStateOf(todayKey) } + var currentMonth by remember { mutableStateOf(YearMonth.from(today)) } + val likedIds by vm.likedDaily.collectAsStateWithLifecycle() + + // 从「每日推荐投稿」跳进来时定位到刚投稿的日期 + LaunchedEffect(Unit) { + vm.pendingSelectDate?.let { date -> + selectedDate = date + runCatching { YearMonth.from(LocalDate.parse(date)) }.getOrNull()?.let { + currentMonth = it + } + vm.pendingSelectDate = null + // 刚投稿成功,重新拉一次列表 + vm.loadDaily() + } + } + + val recommendMap = remember(ui.items) { ui.items.associateBy { it.date } } + val recommendDates = remember(ui.items) { ui.items.map { it.date }.toSet() } + val selected = recommendMap[selectedDate] + + // 网页端进入卡片时会拉一次服务端点赞状态,这里切换日期后同样补一次 + LaunchedEffect(selected?.id) { + selected?.let { vm.loadLikeStatus(it) } + } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding(), + ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AmllTopBar("每日推荐", onBack = nav.back) + + DailyCalendar( + currentMonth = currentMonth, + todayKey = todayKey, + selectedDate = selectedDate, + recommendDates = recommendDates, + onMonthChange = { currentMonth = it }, + onSelectDate = { date -> + selectedDate = date + val month = runCatching { YearMonth.from(LocalDate.parse(date)) }.getOrNull() + if (month != null && month != currentMonth) currentMonth = month + }, + ) + + when { + ui.loading -> DailyCardSkeleton() + selected == null -> { + val emptyDate = runCatching { LocalDate.parse(selectedDate) }.getOrNull() ?: today + EmptyState( + icon = Icons.Rounded.MusicNote, + title = "${emptyDate.year}年${emptyDate.monthValue}月${emptyDate.dayOfMonth}日 暂无推荐", + description = "选择其他日期看看,或成为今天第一个投稿的人", + ) + } + else -> DailyPosterCard( + rec = selected, + liked = selected.id in likedIds, + onLike = { vm.toggleDailyLike(selected) }, + onPlay = { + nav.openLyric( + selected.ncmId, + selected.songName, + selected.artist, + Urls.dailyCover(selected.coverKey, 480), + ) + }, + ) + } + } + } +} + +/* ------------------------------ 整月日历 ------------------------------ */ + +@Composable +private fun DailyCalendar( + currentMonth: YearMonth, + todayKey: String, + selectedDate: String, + recommendDates: Set, + onMonthChange: (YearMonth) -> Unit, + onSelectDate: (String) -> Unit, +) { + val today = remember { LocalDate.now() } + val monthLabel = "${currentMonth.year}年${currentMonth.monthValue}月" + // 与网页端一致:6 行 × 7 列,含上下月补位 + val cells = remember(currentMonth) { buildCalendarCells(currentMonth) } + + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.sheet)) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, RoundedCornerShape(Radius.sheet)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + CalendarNavButton(Icons.Rounded.ChevronLeft, "上个月") { + onMonthChange(currentMonth.minusMonths(1)) + } + Row( + Modifier.weight(1f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(monthLabel, style = Font.section.copy(fontSize = 15.sp), color = Amll.colors.ink) + if (selectedDate != todayKey) { + Spacer(Modifier.width(8.dp)) + Box( + Modifier + .clip(RoundedCornerShape(6.dp)) + .background(Amll.colors.primary) + .clickable { + onMonthChange(YearMonth.from(today)) + onSelectDate(todayKey) + } + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text("今天", style = Font.micro, color = Amll.colors.onPrimary) + } + } + } + CalendarNavButton(Icons.Rounded.ChevronRight, "下个月") { + onMonthChange(currentMonth.plusMonths(1)) + } + } + + Row(Modifier.fillMaxWidth()) { + WEEKDAYS.forEach { label -> + Text( + label, + style = Font.micro.copy(fontWeight = FontWeight.SemiBold), + color = Amll.colors.ink2, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f).padding(vertical = 2.dp), + ) + } + } + + cells.chunked(7).forEach { week -> + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + week.forEach { cell -> + CalendarDayCell( + cell = cell, + isToday = cell.dateKey == todayKey, + isSelected = cell.dateKey == selectedDate, + hasRecommend = cell.dateKey in recommendDates, + onClick = { onSelectDate(cell.dateKey) }, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +private data class CalendarCell(val dateKey: String, val day: Int, val inMonth: Boolean) + +private fun buildCalendarCells(month: YearMonth): List { + val first = month.atDay(1) + // DayOfWeek 以周一为 1,这里换成「周日为 0」的列号 + val leading = first.dayOfWeek.value % 7 + val daysInMonth = month.lengthOfMonth() + val prevMonth = month.minusMonths(1) + val nextMonth = month.plusMonths(1) + val cells = ArrayList(42) + for (i in leading - 1 downTo 0) { + val day = prevMonth.lengthOfMonth() - i + cells += CalendarCell(prevMonth.atDay(day).toString(), day, false) + } + for (day in 1..daysInMonth) { + cells += CalendarCell(month.atDay(day).toString(), day, true) + } + var nextDay = 1 + while (cells.size < 42) { + cells += CalendarCell(nextMonth.atDay(nextDay).toString(), nextDay, false) + nextDay++ + } + return cells +} + +@Composable +private fun CalendarNavButton( + icon: androidx.compose.ui.graphics.vector.ImageVector, + description: String, + onClick: () -> Unit, +) { + Box( + Modifier + .size(30.dp) + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, Amll.colors.border, RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Icon(icon, description, tint = Amll.colors.ink, modifier = Modifier.size(17.dp)) + } +} + +@Composable +private fun CalendarDayCell( + cell: CalendarCell, + isToday: Boolean, + isSelected: Boolean, + hasRecommend: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val bg = when { + isToday -> Amll.colors.primary + isSelected -> Amll.colors.primarySoft + cell.inMonth -> Amll.colors.surface2 + else -> Amll.colors.canvas + } + val borderColor = when { + isToday -> Amll.colors.primary + isSelected -> Amll.colors.primary + hasRecommend -> Amll.colors.success + else -> Amll.colors.border + } + val textColor = when { + isToday -> Amll.colors.onPrimary + !cell.inMonth -> Amll.colors.ink3.copy(alpha = 0.6f) + else -> Amll.colors.ink + } + Box( + modifier + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(bg) + .border(1.dp, borderColor, RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Text( + cell.day.toString(), + style = Font.bodySmall.copy( + fontWeight = if (isToday || isSelected) FontWeight.SemiBold else FontWeight.Normal, + ), + color = textColor, + ) + if (hasRecommend && !isToday) { + Box( + Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 4.dp) + .size(4.dp) + .clip(CircleShape) + .background(Amll.colors.success), + ) + } + } +} + +/* ------------------------------ 推荐卡片 ------------------------------ */ + +@Composable +private fun DotSeparator(modifier: Modifier = Modifier) { + // 颜色在组合阶段取好,Canvas 的绘制 lambda 不是 @Composable 上下文 + val color = Amll.colors.ink3.copy(alpha = 0.5f) + Canvas(modifier.fillMaxWidth().height(3.dp)) { + val radius = 1.5.dp.toPx() + val step = 13.dp.toPx() + var x = 0f + while (x <= size.width) { + drawCircle(color = color, radius = radius, center = Offset(x, size.height / 2)) + x += step + } + } +} + +@Composable +private fun DailyPosterCard( + rec: DailyRecDto, + liked: Boolean, + onLike: () -> Unit, + onPlay: () -> Unit, +) { + val cover = Urls.dailyCover(rec.coverKey, 720) + val date = runCatching { LocalDate.parse(rec.date) }.getOrNull() + val monthAbbr = date?.month?.getDisplayName(TextStyle.SHORT, Locale.US)?.uppercase().orEmpty() + + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.sheet)) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, RoundedCornerShape(Radius.sheet)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + // 封面:正方形海报图,未加载时保持灰底 + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)) + .background(Amll.colors.surface2), + ) { + if (cover != null) { + AsyncImage( + model = cover, + contentDescription = rec.songName, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + rec.songName.ifBlank { "未命名推荐" }, + style = Font.navTitle.copy(fontSize = 18.sp), + color = Amll.colors.ink, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + rec.artist.ifBlank { "未知歌手" }, + style = Font.caption.copy(fontWeight = FontWeight.SemiBold), + color = Amll.colors.ink2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + DotSeparator() + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(18.dp)) { + Column( + Modifier.width(62.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + monthAbbr, + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = Amll.colors.ink2, + ) + Text( + date?.dayOfMonth?.toString() ?: "--", + style = Font.numberXL.copy(fontSize = 40.sp), + color = Amll.colors.ink, + ) + } + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) { + MarkupText(rec.comment) + Text( + "--来自 @${rec.submitter.ifBlank { "匿名用户" }} 的评论", + style = Font.micro, + color = Amll.colors.ink2, + textAlign = TextAlign.End, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + DotSeparator() + + Text( + "AMLL Hub | 今日推荐", + style = Font.caption.copy(fontWeight = FontWeight.Light), + color = Amll.colors.ink2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + Box( + Modifier + .size(48.dp) + .clip(CircleShape) + .background(Amll.colors.ink) + .clickable(onClick = onPlay), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Rounded.PlayArrow, + "播放", + tint = Amll.colors.canvas, + modifier = Modifier.size(24.dp).offset(x = 0.5.dp), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier.clickable(onClick = onLike), + ) { + Icon( + imageVector = if (liked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, + contentDescription = if (liked) "取消点赞" else "点赞", + tint = if (liked) Amll.colors.primary else Amll.colors.ink2, + modifier = Modifier.size(20.dp), + ) + Text( + "${rec.likeCount}", + style = Font.caption, + color = if (liked) Amll.colors.primary else Amll.colors.ink2, + ) + } + } + } +} + +@Composable +private fun DailyCardSkeleton() { + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.sheet)) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, RoundedCornerShape(Radius.sheet)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)) + .background(Amll.colors.surface2), + ) + Box(Modifier.fillMaxWidth(0.6f).height(18.dp).clip(RoundedCornerShape(6.dp)).background(Amll.colors.surface2)) + Box(Modifier.fillMaxWidth(0.35f).height(12.dp).clip(RoundedCornerShape(6.dp)).background(Amll.colors.surface2)) + Box(Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)).background(Amll.colors.surface2)) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitScreen.kt new file mode 100644 index 0000000..ff27fdf --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitScreen.kt @@ -0,0 +1,417 @@ +package dev.amll.hub.android.ui.screen + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Image +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import dev.amll.hub.android.data.remote.NcmSongDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.AmllTextField +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.CoverImage +import dev.amll.hub.android.ui.component.FieldRow +import dev.amll.hub.android.ui.component.MarkupText +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import kotlinx.coroutines.delay +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneOffset + +/* ============================ 每日推荐投稿 ============================ */ + +private val MARKUP_SHORTCUTS = listOf( + "居中" to "[:-:]", + "左对齐" to "[:-]", + "右对齐" to "[-:]", + "小字居中" to "[:_:]", + "小字左" to "[:_]", + "小字右" to "[_:]", +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DailySubmitScreen( + nav: AppNav, + dailyVm: DailyViewModel, + vm: DailySubmitViewModel = hiltViewModel(), +) { + val ui by vm.state.collectAsStateWithLifecycle() + var showDatePicker by remember { mutableStateOf(false) } + + val coverPicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let(vm::pickCover) + } + + // 投稿成功后跳到每日推荐页对应日期(对齐网页端) + LaunchedEffect(ui.successDate) { + val date = ui.successDate ?: return@LaunchedEffect + dailyVm.pendingSelectDate = date + delay(700) + nav.finishSubmitToDaily() + } + + if (showDatePicker) { + val initialMillis = runCatching { + LocalDate.parse(ui.date).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() + }.getOrNull() + val pickerState = rememberDatePickerState(initialSelectedDateMillis = initialMillis) + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + TextButton(onClick = { + pickerState.selectedDateMillis?.let { millis -> + val date = Instant.ofEpochMilli(millis).atZone(ZoneOffset.UTC).toLocalDate() + vm.setDate(date.toString()) + } + showDatePicker = false + }) { Text("确定") } + }, + dismissButton = { + TextButton(onClick = { showDatePicker = false }) { Text("取消") } + }, + ) { DatePicker(state = pickerState) } + } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding(), + ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AmllTopBar("每日推荐投稿", onBack = nav.back) + + // 网易云搜索 + SectionHeader("从网易云搜索填表") + AmllCard(padding = 16.dp) { + AmllTextField( + value = ui.ncmQuery, + onValueChange = vm::setNcmQuery, + placeholder = "输入歌名 / 歌手搜索", + imeAction = ImeAction.Search, + onImeAction = vm::searchNcm, + ) + PrimaryButton( + text = if (ui.ncmSearching) "搜索中…" else "搜索", + onClick = vm::searchNcm, + modifier = Modifier.padding(top = 10.dp), + enabled = ui.ncmQuery.isNotBlank() && !ui.ncmSearching, + ) + ui.ncmResults.forEach { song -> + NcmPickRow(song) { vm.pickNcmSong(song) } + } + } + + // 网易云 ID 解析 + SectionHeader("网易云 ID(可选)") + AmllCard(padding = 16.dp) { + AmllTextField( + value = ui.ncmId, + onValueChange = vm::setNcmId, + placeholder = "歌曲 ID 或歌曲链接", + imeAction = ImeAction.Done, + onImeAction = vm::parseNcmId, + ) + PrimaryButton( + text = if (ui.parseSearching) "解析中…" else "解析并回填", + onClick = vm::parseNcmId, + modifier = Modifier.padding(top = 10.dp), + enabled = ui.ncmId.isNotBlank() && !ui.parseSearching, + ) + } + + SectionHeader("歌曲信息") + AmllCard(padding = 16.dp) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + AmllTextField( + value = ui.songName, + onValueChange = vm::setSongName, + placeholder = "音乐名称(必填)", + ) + AmllTextField( + value = ui.artist, + onValueChange = vm::setArtist, + placeholder = "歌手(必填)", + ) + } + } + + SectionHeader("封面") + AmllCard(padding = 16.dp) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp)) { + Box( + Modifier + .size(104.dp) + .clip(RoundedCornerShape(Radius.field)) + .background(Amll.colors.surface2) + .border(1.dp, Amll.colors.border, RoundedCornerShape(Radius.field)), + contentAlignment = Alignment.Center, + ) { + if (ui.coverPreview.isNotBlank()) { + AsyncImage( + model = ui.coverPreview, + contentDescription = "封面预览", + contentScale = androidx.compose.ui.layout.ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } else { + Icon( + Icons.Rounded.Image, + null, + tint = Amll.colors.ink3, + modifier = Modifier.size(28.dp), + ) + } + } + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + when { + ui.coverUploading -> "上传中…" + ui.coverTempKey.isNotBlank() -> "上传完成" + else -> "建议上传正方形封面" + }, + style = Font.caption, + color = if (ui.coverTempKey.isNotBlank()) Amll.colors.success else Amll.colors.ink3, + ) + if (ui.coverError.isNotBlank()) { + Text(ui.coverError, style = Font.micro, color = Amll.colors.error) + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + SmallAction("从相册选择", Amll.colors.ink2) { coverPicker.launch("image/*") } + if (ui.coverPreview.isNotBlank()) { + SmallAction("删除", Amll.colors.warning, vm::removeCover) + } + } + } + } + } + + SectionHeader("推荐日期") + AmllCard(padding = 16.dp) { + FieldRow("日期", ui.date, onClick = { showDatePicker = true }) + Text( + when { + ui.dateChecking -> "正在检查日期可用性…" + !ui.dateAvailable -> "该日期已被占用,请选择其他日期" + else -> "该日期可用" + }, + style = Font.micro, + color = when { + ui.dateChecking -> Amll.colors.ink3 + !ui.dateAvailable -> Amll.colors.error + else -> Amll.colors.success + }, + modifier = Modifier.padding(top = 8.dp), + ) + } + + SectionHeader("推荐语") + CommentField( + comment = ui.comment, + onCommentChange = vm::setComment, + ) + if (ui.comment.trim().isNotEmpty()) { + Text("实时预览:", style = Font.micro, color = Amll.colors.ink3) + AmllCard(padding = 16.dp) { MarkupText(ui.comment, emptyText = "") } + } + + ui.message?.let { + Text( + it, + style = Font.caption, + color = if (ui.messageIsError) Amll.colors.error else Amll.colors.success, + ) + } + + PrimaryButton( + text = if (ui.submitting) "提交中…" else "立即投稿", + onClick = vm::submit, + enabled = ui.canSubmit, + ) + } + } +} + +@Composable +private fun NcmPickRow(song: NcmSongDto, onClick: () -> Unit) { + Row( + Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(top = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + CoverImage(song.displayCover.ifBlank { null }, song.displayName, 40.dp) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + song.displayName, + style = Font.bodySmall, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + song.displayArtist, + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun SmallAction(label: String, color: androidx.compose.ui.graphics.Color, onClick: () -> Unit) { + Box( + Modifier + .clip(RoundedCornerShape(999.dp)) + .border(1.dp, Amll.colors.border, RoundedCornerShape(999.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) { + Text(label, style = Font.micro, color = color) + } +} + +/** 推荐语输入:保留 TextFieldValue 以便标记按钮在光标处插入 */ +@Composable +private fun CommentField( + comment: String, + onCommentChange: (String) -> Unit, +) { + var value by remember { mutableStateOf(TextFieldValue(comment, TextRange(comment.length))) } + // 外部(清空/提交后)变化时同步回来 + LaunchedEffect(comment) { + if (comment != value.text) value = TextFieldValue(comment, TextRange(comment.length)) + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + MARKUP_SHORTCUTS.take(3).forEach { (label, markup) -> + SmallAction(label, Amll.colors.ink2) { + val next = insertMarkup(value, markup) + value = next + onCommentChange(next.text) + } + } + } + Row( + Modifier.fillMaxWidth().padding(top = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + MARKUP_SHORTCUTS.drop(3).forEach { (label, markup) -> + SmallAction(label, Amll.colors.ink2) { + val next = insertMarkup(value, markup) + value = next + onCommentChange(next.text) + } + } + } + + OutlinedTextField( + value = value, + onValueChange = { next -> + val capped = if (next.text.length > MAX_COMMENT) { + val end = next.selection.max.coerceAtMost(MAX_COMMENT) + TextFieldValue(next.text.take(MAX_COMMENT), TextRange(end)) + } else { + next + } + value = capped + onCommentChange(capped.text) + }, + placeholder = { Text("输入推荐语,可使用上方标记按钮排版", style = Font.bodySmall, color = Amll.colors.ink3) }, + minLines = 6, + maxLines = 12, + textStyle = Font.body, + shape = RoundedCornerShape(Radius.field), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Amll.colors.surface2, + unfocusedContainerColor = Amll.colors.surface2, + focusedTextColor = Amll.colors.ink, + unfocusedTextColor = Amll.colors.ink, + cursorColor = Amll.colors.primary, + focusedBorderColor = Amll.colors.primary, + unfocusedBorderColor = Amll.colors.border, + ), + ) + Text( + "${value.text.length} / 7000", + style = Font.micro, + color = Amll.colors.ink3, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + ) +} + +private const val MAX_COMMENT = 7000 + +private fun insertMarkup(value: TextFieldValue, markup: String): TextFieldValue { + val start = value.selection.min + val end = value.selection.max + val text = value.text + val next = text.substring(0, start) + markup + text.substring(start) + val cursor = (start + markup.length + (end - start)).coerceAtMost(next.length) + return TextFieldValue(next, TextRange(cursor)) +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitViewModel.kt new file mode 100644 index 0000000..bc7de4d --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailySubmitViewModel.kt @@ -0,0 +1,308 @@ +package dev.amll.hub.android.ui.screen + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.amll.hub.android.data.remote.CreateDailyRecRequest +import dev.amll.hub.android.data.remote.NcmSongDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDate +import javax.inject.Inject + +/** + * 每日推荐投稿:对齐网页端 DailyRecommendForm + * (网易云搜索回填 / 封面自动抓取或相册上传 / 日期查重 / 推荐语标记 / 提交)。 + */ +@HiltViewModel +class DailySubmitViewModel @Inject constructor( + private val repo: ContentRepository, + @ApplicationContext private val context: Context, +) : ViewModel() { + + data class UiState( + val date: String = LocalDate.now().toString(), + val dateChecking: Boolean = false, + val dateAvailable: Boolean = true, + val ncmQuery: String = "", + val ncmSearching: Boolean = false, + val ncmResults: List = emptyList(), + val parseSearching: Boolean = false, + val songName: String = "", + val artist: String = "", + val ncmId: String = "", + /** 预览地址:相册内容 URI 或网易云远程 URL */ + val coverPreview: String = "", + val coverTempKey: String = "", + val coverUploading: Boolean = false, + val coverError: String = "", + val comment: String = "", + val submitting: Boolean = false, + val message: String? = null, + val messageIsError: Boolean = false, + val successDate: String? = null, + ) { + val canSubmit: Boolean + get() = date.isNotBlank() && + dateAvailable && + songName.isNotBlank() && + artist.isNotBlank() && + comment.isNotBlank() && + coverTempKey.isNotBlank() && + !coverUploading && + !submitting + } + + private val _state = MutableStateFlow(UiState()) + val state: StateFlow = _state.asStateFlow() + + private var dateJob: Job? = null + private var searchJob: Job? = null + + init { + checkDate(_state.value.date) + } + + fun setDate(value: String) { + _state.update { it.copy(date = value, message = null) } + checkDate(value) + } + + fun setNcmQuery(value: String) = + _state.update { it.copy(ncmQuery = value, message = null) } + + fun setSongName(value: String) = _state.update { it.copy(songName = value) } + + fun setArtist(value: String) = _state.update { it.copy(artist = value) } + + fun setNcmId(value: String) = _state.update { it.copy(ncmId = value) } + + fun setComment(value: String) = _state.update { it.copy(comment = value) } + + fun dismissMessage() = _state.update { it.copy(message = null) } + + fun searchNcm() { + val keyword = _state.value.ncmQuery.trim() + if (keyword.isEmpty()) return + searchJob?.cancel() + _state.update { it.copy(ncmSearching = true, message = null) } + searchJob = viewModelScope.launch { + repo.ncmSearch(keyword, limit = 8) + .onSuccess { songs -> + _state.update { + it.copy( + ncmSearching = false, + ncmResults = songs, + message = if (songs.isEmpty()) "没有找到相关歌曲" else null, + messageIsError = songs.isEmpty(), + ) + } + } + .onFailure { e -> + _state.update { + it.copy( + ncmSearching = false, + message = e.message ?: "搜索失败", + messageIsError = true, + ) + } + } + } + } + + /** 选中搜索结果:回填并自动抓取封面 */ + fun pickNcmSong(song: NcmSongDto) { + _state.update { + it.copy( + songName = song.displayName, + artist = song.displayArtist, + ncmId = song.displayId, + ncmQuery = "", + ncmResults = emptyList(), + message = null, + ) + } + val cover = song.displayCover + if (cover.isNotBlank()) fetchCoverFromUrl(cover) + } + + /** 通过 ID / 链接解析歌曲信息并自动抓取封面 */ + fun parseNcmId() { + val id = extractSongId(_state.value.ncmId) + if (id.isBlank()) { + _state.update { it.copy(message = "请输入有效的网易云 ID 或歌曲链接", messageIsError = true) } + return + } + _state.update { it.copy(parseSearching = true, message = null) } + viewModelScope.launch { + repo.parseMusic(id, level = "standard") + .onSuccess { music -> + _state.update { + it.copy( + parseSearching = false, + songName = music.displayName.ifBlank { music.songName }, + artist = music.displayArtist.ifBlank { music.artistName }, + ncmId = id, + ) + } + val cover = music.displayCover + if (cover.isNotBlank()) fetchCoverFromUrl(cover) + } + .onFailure { e -> + _state.update { + it.copy( + parseSearching = false, + message = e.message ?: "解析失败", + messageIsError = true, + ) + } + } + } + } + + /** 相册选图:先预览再上传临时区 */ + fun pickCover(uri: Uri) { + _state.update { + it.copy( + coverPreview = uri.toString(), + coverTempKey = "", + coverUploading = true, + coverError = "", + message = null, + ) + } + viewModelScope.launch { + val bytes = withContext(Dispatchers.IO) { + runCatching { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } } + .getOrNull() + } + if (bytes == null) { + _state.update { it.copy(coverUploading = false, coverError = "读取图片失败") } + return@launch + } + val name = displayName(uri) + val mime = context.contentResolver.getType(uri).orEmpty().ifBlank { "image/jpeg" } + repo.uploadDailyCover(name, bytes, mime) + .onSuccess { key -> + _state.update { + it.copy(coverTempKey = key, coverUploading = false, coverError = "") + } + } + .onFailure { e -> + _state.update { + it.copy( + coverUploading = false, + coverError = e.message ?: "封面上传失败", + ) + } + } + } + } + + fun removeCover() { + _state.update { + it.copy(coverPreview = "", coverTempKey = "", coverUploading = false, coverError = "") + } + } + + fun submit() { + val current = _state.value + if (!current.canSubmit) return + viewModelScope.launch { + _state.update { it.copy(submitting = true, message = null) } + repo.createDailyRecommendation( + CreateDailyRecRequest( + songName = current.songName.trim(), + artist = current.artist.trim(), + coverTempKey = current.coverTempKey, + date = current.date, + comment = current.comment, + ncmId = current.ncmId.trim().takeIf { it.isNotEmpty() }, + ), + ) + .onSuccess { id -> + _state.update { + it.copy( + submitting = false, + message = "投稿成功!", + messageIsError = false, + successDate = current.date, + ) + } + } + .onFailure { e -> + _state.update { + it.copy( + submitting = false, + message = e.message ?: "投稿失败", + messageIsError = true, + ) + } + } + } + } + + private fun fetchCoverFromUrl(url: String) { + _state.update { + it.copy( + coverPreview = url, + coverTempKey = "", + coverUploading = true, + coverError = "", + message = null, + ) + } + viewModelScope.launch { + repo.uploadDailyCoverFromUrl(url) + .onSuccess { key -> + _state.update { + it.copy(coverTempKey = key, coverUploading = false, coverError = "") + } + } + .onFailure { e -> + _state.update { + it.copy( + coverUploading = false, + coverError = e.message ?: "封面自动载入失败", + message = "封面自动载入失败,请手动上传", + messageIsError = true, + ) + } + } + } + } + + private fun checkDate(date: String) { + dateJob?.cancel() + _state.update { it.copy(dateChecking = true) } + dateJob = viewModelScope.launch { + repo.checkDailyDate(date) + .onSuccess { available -> + _state.update { it.copy(dateChecking = false, dateAvailable = available) } + } + .onFailure { + // 与网页端一致:查重失败不阻塞投稿 + _state.update { it.copy(dateChecking = false, dateAvailable = true) } + } + } + } + + private fun displayName(uri: Uri): String { + val fromResolver = runCatching { + context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) + ?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null } + }.getOrNull() + return fromResolver?.takeIf { it.isNotBlank() } ?: "cover.jpg" + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyViewModel.kt new file mode 100644 index 0000000..79c3033 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DailyViewModel.kt @@ -0,0 +1,96 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DailyViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val daily = MutableStateFlow(ListUi()) + val likedDaily = MutableStateFlow>(emptySet()) + + /** 每日推荐投稿成功后要定位的日期;由 DailyScreen 进入时消费 */ + var pendingSelectDate: String? = null + + fun loadDaily() { + viewModelScope.launch { + daily.update { it.copy(loading = true, error = null) } + val result = repo.daily() + val items = result.getOrDefault(emptyList()).sortedByDescending { it.date } + daily.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无推荐" else null, + ) + } + } + } + + /** 进入某张卡片时同步服务端点赞状态(与网页端一致) */ + fun loadLikeStatus(rec: DailyRecDto) { + if (rec.id == 0L) return + viewModelScope.launch { + repo.likeStatus(rec.id).onSuccess { status -> + likedDaily.update { if (status.liked) it + rec.id else it - rec.id } + daily.update { ui -> + ui.copy( + items = ui.items.map { + if (it.id == rec.id) it.copy(likeCount = status.likeCount) else it + }, + ) + } + } + } + } + + fun toggleDailyLike(rec: DailyRecDto) { + if (rec.id == 0L) return + val liked = rec.id in likedDaily.value + // 先本地翻转,失败再回滚 + likedDaily.update { if (liked) it - rec.id else it + rec.id } + daily.update { ui -> + ui.copy( + items = ui.items.map { + if (it.id == rec.id) { + it.copy(likeCount = (it.likeCount + if (liked) -1 else 1).coerceAtLeast(0)) + } else { + it + } + }, + ) + } + viewModelScope.launch { + val result = repo.toggleLike(rec.id).getOrNull() + if (result == null) { + likedDaily.update { if (liked) it + rec.id else it - rec.id } + daily.update { ui -> + ui.copy( + items = ui.items.map { + if (it.id == rec.id) { + it.copy(likeCount = (it.likeCount + if (liked) 1 else -1).coerceAtLeast(0)) + } else { + it + } + }, + ) + } + } else { + likedDaily.update { if (result.liked) it + rec.id else it - rec.id } + daily.update { ui -> + ui.copy(items = ui.items.map { if (it.id == rec.id) it.copy(likeCount = result.likeCount) else it }) + } + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/DiscoverScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DiscoverScreen.kt new file mode 100644 index 0000000..8a72d96 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/DiscoverScreen.kt @@ -0,0 +1,169 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.TabScaffold +import dev.amll.hub.android.ui.bottomBarPadding +import dev.amll.hub.android.ui.component.PageHeader +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.component.SongRow +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.CardShape +import dev.amll.hub.android.ui.theme.Font + +/* ============================ 02 发现 ============================ */ + +@Composable +fun DiscoverScreen(vm: HomeViewModel, nav: AppNav) { + LaunchedEffect(Unit) { if (vm.home.value.latest.isEmpty()) vm.loadHome() } + val ui by vm.home.collectAsStateWithLifecycle() + + TabScaffold { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 20.dp, + end = 20.dp, + top = 10.dp, + bottom = bottomBarPadding(), + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { PageHeader("发现", "歌词、榜单与每日推荐") } + item { SearchField("搜索站内歌词库", onClick = nav.openSearch) } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + DiscoverCard( + title = "歌词搜索", + desc = "按歌名、歌手、歌词片段检索词库", + modifier = Modifier.weight(1f), + onClick = nav.openSearch, + ) + DiscoverCard( + title = "每日推荐", + desc = "看看今天大家都在听什么", + modifier = Modifier.weight(1f), + onClick = nav.openDaily, + ) + } + } + + item { + DiscoverCard( + title = "网易云解析", + desc = "粘贴歌曲 ID / 链接,或按关键词搜到就能直接播", + modifier = Modifier.fillMaxWidth(), + onClick = nav.openNcm, + ) + } + + // 标题与内容同属一个区块,内部 8dp 小于区块间的 16dp + item { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + SectionHeader("无歌词排行榜", action = "查看榜单", onAction = nav.openRanking) + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + ui.ranking.take(2).forEachIndexed { index, item -> + RankingCompactRow(rank = index + 1, item = item, onClick = nav.openRanking) + } + if (ui.ranking.isEmpty()) { + Text("暂无榜单数据", style = Font.caption, color = Amll.colors.ink3) + } + } + } + } + + item { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + SectionHeader("最新收录", action = "查看全部", onAction = nav.openLatest) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + ui.latest.take(6).forEach { song -> + SongRow( + title = song.title, + subtitle = song.artist, + coverUrl = song.coverUrl.ifBlank { null }, + coverSize = 40.dp, + onClick = { + nav.openLyric(song.ncmId, song.title, song.artist, song.coverUrl) + }, + ) + } + } + } + } + } + } +} + +@Composable +private fun DiscoverCard( + title: String, + desc: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + Column( + modifier + .height(96.dp) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .clip(CardShape) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, CardShape) + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(title, style = Font.cardTitle.copy(fontSize = 14.sp), color = Amll.colors.ink) + Text(desc, style = Font.micro, color = Amll.colors.ink3, maxLines = 2, overflow = TextOverflow.Ellipsis) + } +} + +@Composable +private fun RankingCompactRow(rank: Int, item: RankingItemDto, onClick: () -> Unit) { + Row( + Modifier.fillMaxWidth().clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + "$rank", + style = Font.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = if (rank == 1) Amll.colors.primary else Amll.colors.ink, + modifier = Modifier.width(16.dp), + ) + Text( + item.songName.ifBlank { "未知歌曲" }, + style = Font.body, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Text("${item.requestCount} 次", style = Font.micro, color = Amll.colors.ink3) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeScreen.kt index 7975248..d710560 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeScreen.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeScreen.kt @@ -1,133 +1,445 @@ package dev.amll.hub.android.ui.screen +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material.icons.filled.QrCodeScanner -import androidx.compose.material3.Button -import androidx.compose.material3.Card +import androidx.compose.material.icons.outlined.AddCircleOutline +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material.icons.outlined.Explore +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.PersonOutline +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.QrCodeScanner +import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import dev.amll.hub.android.data.remote.UserProfile +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.data.remote.Urls +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.TabScaffold +import dev.amll.hub.android.ui.bottomBarPadding +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.CoverImage +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.component.SongRow +import dev.amll.hub.android.ui.component.TabItem +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.HeroShape +import dev.amll.hub.android.ui.theme.Radius + +val AppTabs = listOf( + TabItem("首页", Icons.Outlined.Home), + TabItem("发现", Icons.Outlined.Explore), + TabItem("创作", Icons.Outlined.AddCircleOutline), + TabItem("消息", Icons.Outlined.ChatBubbleOutline), + TabItem("我的", Icons.Outlined.PersonOutline), +) -/** - * 登录后的落地页(当前阶段用于展示登录态与入口,后续接入个人中心)。 - */ @Composable -fun HomeScreen( - profile: UserProfile?, - onScanQr: () -> Unit, - onLoggedOut: () -> Unit, - onRefreshProfile: () -> Unit, +fun SearchField( + placeholder: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, ) { - // 登录态已恢复但资料缺失(如离线启动)时补拉一次 - LaunchedEffect(profile) { - if (profile == null) onRefreshProfile() + Row( + modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .clip(RoundedCornerShape(15.dp)) + .background(Amll.colors.surface2) + // 上下留白取左右的一半,比原来 46dp 定高更紧凑 + .padding(horizontal = 14.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Icon(Icons.Rounded.Search, null, tint = Amll.colors.ink3, modifier = Modifier.size(18.dp)) + Text(placeholder, style = Font.bodySmall, color = Amll.colors.ink3) } +} - Surface(color = MaterialTheme.colorScheme.background) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), +/* ============================ 01 首页 ============================ */ + +@Composable +fun HomeScreen(vm: HomeViewModel, profileVm: ProfileViewModel, nav: AppNav) { + LaunchedEffect(Unit) { + if (vm.home.value.latest.isEmpty()) vm.loadHome() + profileVm.loadProfile() + } + val ui by vm.home.collectAsStateWithLifecycle() + val profile by profileVm.profile.collectAsStateWithLifecycle() + + TabScaffold { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 20.dp, + end = 20.dp, + top = 10.dp, + bottom = bottomBarPadding(), + ), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Spacer(Modifier.height(16.dp)) - Text( - text = "AMLL Hub", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.height(24.dp)) + item { + HomeHeader( + avatar = profile?.avatar, + onScan = nav.openScan, + ) + } - profile?.let { ProfileCard(it) } + item { SearchField("搜歌名、歌手,或粘贴歌词片段", onClick = nav.openSearch) } - Spacer(Modifier.height(24.dp)) + item { + if (ui.loading) { + TodayHeroSkeleton() + } else { + TodayHero(ui.today) { + val rec = ui.today + if (rec != null) { + nav.openLyric(rec.ncmId, rec.songName, rec.artist, Urls.dailyCover(rec.coverKey, 480)) + } else { + nav.openDaily() + } + } + } + } - Button( - onClick = onScanQr, - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - imageVector = Icons.Filled.QrCodeScanner, - contentDescription = null, - modifier = Modifier.size(18.dp), + item { SectionHeader("最新收录", action = "查看全部", onAction = nav.openLatest) } + + when { + ui.loading -> item { SkeletonList(3) } + ui.latest.isEmpty() -> item { + Text("暂无最新收录", style = Font.bodySmall, color = Amll.colors.ink3) + } + // 歌曲之间 2dp:外层 16dp 的 section 间距会把列表拉得很散 + else -> item { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + ui.latest.take(6).forEach { song -> + SongRow( + title = song.title, + subtitle = song.artist, + coverUrl = song.coverUrl.ifBlank { null }, + onClick = { + nav.openLyric(song.ncmId, song.title, song.artist, song.coverUrl) + }, + ) + } + } + } + } + + item { + SectionHeader( + title = "请求数最高的待补歌词", + action = "查看完整排行", + onAction = nav.openRanking, ) - Spacer(Modifier.size(8.dp)) - Text("扫码登录网页端") } - Spacer(Modifier.height(8.dp)) + item { + when { + ui.loading -> SkeletonList(3) + ui.ranking.isEmpty() -> AmllCard(padding = 14.dp) { + Text( + "最近 7 天没有缺歌词请求", + style = Font.caption, + color = Amll.colors.ink3, + ) + } + else -> AmllCard(padding = 6.dp) { + ui.ranking.take(5).forEachIndexed { index, entry -> + HomeRankRow(rank = index + 1, item = entry) { + nav.openLyric( + entry.platformId, + entry.songName, + entry.artists.joinToString("/"), + entry.cover, + ) + } + } + } + } + } + } + } +} - OutlinedButton( - onClick = onLoggedOut, - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Logout, - contentDescription = null, - modifier = Modifier.size(18.dp), +/** 首页排行榜行:序号 + 封面 + 歌名/歌手 + 请求次数(对齐网页端 NotFoundRanking) */ +@Composable +private fun HomeRankRow(rank: Int, item: RankingItemDto, onClick: () -> Unit) { + Row( + Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + "$rank", + style = Font.bodyMedium.copy(fontSize = 17.sp, fontWeight = FontWeight.Bold), + color = if (rank == 1) Amll.colors.primary else Amll.colors.ink3, + modifier = Modifier.width(16.dp), + ) + CoverImage(item.cover.ifBlank { null }, item.songName, 40.dp, radius = Radius.field) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + item.songName.ifBlank { "未知歌曲" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (item.artists.isNotEmpty()) { + Text( + item.artists.joinToString(" / "), + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.size(8.dp)) - Text("退出登录") } } + Text( + "${item.requestCount} 次请求", + style = Font.micro, + color = Amll.colors.ink2, + ) + } +} + +@Composable +private fun HomeHeader( + avatar: String?, + onScan: () -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("AMLL Hub", style = Font.pageTitle, color = Amll.colors.ink, modifier = Modifier.weight(1f)) + Icon( + Icons.Rounded.QrCodeScanner, + "扫一扫", + tint = Amll.colors.ink, + modifier = Modifier.size(21.dp).clickable(onClick = onScan), + ) + Spacer(Modifier.width(14.dp)) + Avatar(avatar, 32.dp) + } +} + +/** 头像:加载中/无地址时统一显示灰色底,与网页端一致 */ +@Composable +fun Avatar(url: String?, size: androidx.compose.ui.unit.Dp, modifier: Modifier = Modifier) { + val model = Urls.absolute(url) + Box( + modifier + .size(size) + .clip(CircleShape) + .background(Amll.colors.surface2), + ) { + if (model != null) { + AsyncImage( + model = model, + contentDescription = "头像", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } } } @Composable -private fun ProfileCard(profile: UserProfile) { - Card(modifier = Modifier.fillMaxWidth()) { +private fun TodayHero(today: DailyRecDto?, onClick: () -> Unit) { + val cover = Urls.dailyCover(today?.coverKey, 480) + Box( + Modifier + .fillMaxWidth() + .height(168.dp) + .clip(HeroShape) + .clickable(onClick = onClick), + ) { + // 背景:封面高斯模糊;拿不到封面时用灰色底兜底 + Box( + Modifier + .fillMaxSize() + .background(Amll.colors.surface2), + ) + if (cover != null) { + AsyncImage( + model = cover, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .then( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Modifier.blur(26.dp) + } else { + Modifier + }, + ), + ) + } + // 压一层暗角,保证左侧白字在任何封面上都可读 + Box( + Modifier + .fillMaxSize() + .background( + Brush.horizontalGradient( + listOf(Color.Black.copy(alpha = 0.78f), Color.Black.copy(alpha = 0.42f)), + ), + ), + ) + Row( + Modifier.fillMaxSize().padding(16.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), ) { - if (profile.avatar.isNotBlank()) { - AsyncImage( - model = profile.avatar, - contentDescription = "头像", - contentScale = ContentScale.Crop, - modifier = Modifier.size(56.dp), - ) - Spacer(Modifier.size(8.dp)) + Box( + Modifier + .size(104.dp) + .clip(RoundedCornerShape(18.dp)) + .background(Color.White.copy(alpha = 0.12f)), + ) { + if (cover != null) { + AsyncImage( + model = cover, + contentDescription = today?.songName, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } } - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = profile.displayName.ifBlank { profile.name }, - style = MaterialTheme.typography.titleMedium, - ) - if (profile.email.isNotBlank()) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(7.dp)) { + Box( + Modifier + .clip(RoundedCornerShape(10.dp)) + .background(Color.White.copy(alpha = 0.22f)) + .padding(horizontal = 9.dp, vertical = 4.dp), + ) { Text( - text = profile.email, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + "今日推荐", + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = Color.White, ) } - if (profile.isAdmin || profile.isReviewer) { - Text( - text = if (profile.isAdmin) "超级管理员" else "审核员", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, + Text( + today?.songName ?: "今日暂无推荐", + style = Font.navTitle.copy(fontSize = 19.sp), + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + today?.let { "${it.artist} · 来自 @${it.submitter}" } ?: "成为今天第一个投稿的人", + style = Font.caption, + color = Color.White.copy(alpha = 0.82f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (today != null) { + Box( + Modifier + .size(38.dp) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Rounded.PlayArrow, + "播放", + tint = Amll.colors.primary, + // PlayArrow 字形在视口内略偏右,仅做 0.5dp 的视觉回正, + // 避免整格居中后因为三角形重心靠左而显得偏左 + modifier = Modifier.size(28.dp).offset(x = (-0.5).dp), ) } } } } } + +/** 今日推荐加载态:与 TodayHero 同尺寸,避免加载时显示「今日暂无推荐」 */ +@Composable +private fun TodayHeroSkeleton() { + Box( + Modifier + .fillMaxWidth() + .height(168.dp) + .clip(HeroShape) + .background(Amll.colors.surface2), + ) { + Row( + Modifier.fillMaxSize().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Box( + Modifier + .size(104.dp) + .clip(RoundedCornerShape(18.dp)) + .background(Amll.colors.surface), + ) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Box( + Modifier + .width(56.dp) + .height(18.dp) + .clip(RoundedCornerShape(9.dp)) + .background(Amll.colors.surface), + ) + Box( + Modifier + .fillMaxWidth(0.7f) + .height(18.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Amll.colors.surface), + ) + Box( + Modifier + .fillMaxWidth(0.5f) + .height(12.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Amll.colors.surface), + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeViewModel.kt new file mode 100644 index 0000000..96961f8 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/HomeViewModel.kt @@ -0,0 +1,55 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.DailyRecDto +import dev.amll.hub.android.data.remote.LatestSongDto +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.data.remote.StatsDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class HomeUi( + val loading: Boolean = true, + val stats: StatsDto? = null, + val today: DailyRecDto? = null, + val latest: List = emptyList(), + val ranking: List = emptyList(), + val error: String? = null, +) + +/** 首页 / 发现 / 最新收录共用:同一份内容缓存,切 Tab 不重复请求 */ +@HiltViewModel +class HomeViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val home = MutableStateFlow(HomeUi()) + + fun loadHome() { + viewModelScope.launch { + home.update { it.copy(loading = true, error = null) } + val stats = repo.stats() + val today = repo.today() + val latest = repo.latest() + val ranking = repo.ranking(7) + home.update { + it.copy( + loading = false, + stats = stats.getOrNull(), + today = today.getOrNull(), + // 首页只展示前几条,完整列表在「最新收录」页 + latest = latest.getOrDefault(emptyList()), + // 首页排行榜展示前 5 条,与网页端 NotFoundRanking 一致 + ranking = ranking.getOrDefault(emptyList()).take(5), + error = latest.exceptionOrNull()?.message + ?: stats.exceptionOrNull()?.message, + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LatestSongsScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LatestSongsScreen.kt new file mode 100644 index 0000000..cb1a3a0 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LatestSongsScreen.kt @@ -0,0 +1,79 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.component.SongRow +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font + +/** + * 最新收录全量列表:首页/发现只放前几条,这里展示接口返回的全部。 + */ +@Composable +fun LatestSongsScreen(vm: HomeViewModel, nav: AppNav) { + LaunchedEffect(Unit) { if (vm.home.value.latest.isEmpty()) vm.loadHome() } + val ui by vm.home.collectAsStateWithLifecycle() + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 20.dp, end = 20.dp, top = 6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + AmllTopBar("最新收录", onBack = nav.back) { + Text( + if (ui.latest.isEmpty()) "" else "共 ${ui.latest.size} 首", + style = Font.caption, + color = Amll.colors.ink3, + ) + } + + when { + ui.loading -> Column(Modifier.fillMaxWidth()) { SkeletonList(6) } + ui.latest.isEmpty() -> EmptyState( + icon = Icons.Outlined.Inbox, + title = "暂无最新收录", + description = "等第一首歌词通过审核后会出现在这里", + ) + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(top = 4.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + items(ui.latest) { item -> + SongRow( + title = item.title, + subtitle = item.artist, + coverUrl = item.coverUrl.ifBlank { null }, + onClick = { nav.openLyric(item.ncmId, item.title, item.artist, item.coverUrl) }, + ) + } + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ListUi.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ListUi.kt new file mode 100644 index 0000000..90855c8 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ListUi.kt @@ -0,0 +1,8 @@ +package dev.amll.hub.android.ui.screen + +/** 通用列表状态:区分「加载中 / 成功但为空 / 请求失败」 */ +data class ListUi( + val loading: Boolean = true, + val items: List = emptyList(), + val error: String? = null, +) diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginScreen.kt index 7845f1a..180d80a 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginScreen.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginScreen.kt @@ -1,6 +1,10 @@ package dev.amll.hub.android.ui.screen +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -8,42 +12,50 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.R import dev.amll.hub.android.data.remote.UserProfile +import dev.amll.hub.android.ui.component.AmllTextField +import dev.amll.hub.android.ui.component.CaptchaDialog import dev.amll.hub.android.ui.component.ErrorBanner +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.SegmentedTabs +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +/** + * 登录页:与 App 其余页面共用同一套颜色/字号/圆角 token, + * 发码流程与网页端一致(check-user → 人机验证 → send-code)。 + */ @Composable fun LoginScreen( onLoggedIn: (UserProfile) -> Unit, @@ -51,189 +63,204 @@ fun LoginScreen( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - Surface(color = MaterialTheme.colorScheme.background) { + Box(Modifier.fillMaxSize().background(Amll.colors.canvas)) { Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) + .statusBarsPadding() + .navigationBarsPadding() .imePadding() - .padding(horizontal = 28.dp, vertical = 40.dp), + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(Modifier.height(24.dp)) + BrandMark() + Spacer(Modifier.height(26.dp)) Text( - text = "AMLL Hub", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary, + "登录 AMLL Hub", + style = Font.pageTitle.copy(fontSize = 22.sp), + color = Amll.colors.ink, ) Spacer(Modifier.height(6.dp)) Text( - text = "登录后即可投稿歌词、管理个人资料", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + "登录后即可投稿歌词、参与审核", + style = Font.caption, + color = Amll.colors.ink3, textAlign = TextAlign.Center, ) - Spacer(Modifier.height(32.dp)) - - // 密码 / 验证码切换 - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - SegmentedButton( - selected = state.tab == LoginTab.PASSWORD, - onClick = { viewModel.switchTab(LoginTab.PASSWORD) }, - shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), - ) { Text("密码登录") } - SegmentedButton( - selected = state.tab == LoginTab.CODE, - onClick = { viewModel.switchTab(LoginTab.CODE) }, - shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), - ) { Text("验证码登录") } - } + Spacer(Modifier.height(26.dp)) + + SegmentedTabs( + labels = listOf("密码登录", "验证码登录"), + selectedIndex = if (state.tab == LoginTab.PASSWORD) 0 else 1, + onSelect = { index -> + viewModel.switchTab(if (index == 0) LoginTab.PASSWORD else LoginTab.CODE) + }, + ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(18.dp)) state.error?.let { message -> ErrorBanner(message = message) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(12.dp)) } when (state.tab) { - LoginTab.PASSWORD -> PasswordForm(state, viewModel) - LoginTab.CODE -> CodeForm(state, viewModel) + LoginTab.PASSWORD -> PasswordForm(state, viewModel) { + viewModel.submit { onLoggedIn(it.user) } + } + LoginTab.CODE -> CodeForm(state, viewModel) { + viewModel.submit { onLoggedIn(it.user) } + } } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(22.dp)) - Button( + PrimaryButton( + text = if (state.submitting) "登录中…" else "登录", + enabled = state.canSubmit && !state.submitting, onClick = { viewModel.submit { onLoggedIn(it.user) } }, - enabled = state.canSubmit, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - ) { - if (state.submitting) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary, - ) - } else { - Text("登录") - } - } + ) + + Spacer(Modifier.height(14.dp)) + Text( + "登录即表示同意《用户协议》与《隐私政策》", + style = Font.micro, + color = Amll.colors.ink3, + textAlign = TextAlign.Center, + ) + } + + val captcha = state.captcha + if (captcha != null) { + CaptchaDialog( + config = captcha, + verified = state.captchaVerified, + sending = state.sendingCode, + error = state.error, + onToken = viewModel::onCaptchaToken, + onDismiss = viewModel::dismissCaptcha, + ) } } } @Composable -private fun PasswordForm(state: LoginUiState, viewModel: LoginViewModel) { - Column(modifier = Modifier.fillMaxWidth()) { - OutlinedTextField( +private fun BrandMark() { + Image( + painter = painterResource(R.drawable.ic_launcher_logo), + contentDescription = null, + modifier = Modifier.size(68.dp).clip(RoundedCornerShape(16.dp)), + ) +} + +@Composable +private fun PasswordForm( + state: LoginUiState, + viewModel: LoginViewModel, + onSubmit: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + AmllTextField( value = state.username, onValueChange = viewModel::onUsernameChange, - label = { Text("用户名") }, - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - modifier = Modifier.fillMaxWidth(), + label = "用户名", + placeholder = "请输入用户名", + imeAction = ImeAction.Next, ) - Spacer(Modifier.height(12.dp)) - OutlinedTextField( + AmllTextField( value = state.password, onValueChange = viewModel::onPasswordChange, - label = { Text("密码") }, - singleLine = true, + label = "密码", + placeholder = "请输入密码", + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, visualTransformation = if (state.passwordVisible) { VisualTransformation.None } else { PasswordVisualTransformation() }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - trailingIcon = { - IconButton(onClick = viewModel::togglePasswordVisible) { - Icon( - imageVector = if (state.passwordVisible) { - Icons.Filled.VisibilityOff - } else { - Icons.Filled.Visibility - }, - contentDescription = if (state.passwordVisible) "隐藏密码" else "显示密码", - ) - } + onImeAction = onSubmit, + trailing = { + Icon( + imageVector = if (state.passwordVisible) { + Icons.Filled.VisibilityOff + } else { + Icons.Filled.Visibility + }, + contentDescription = if (state.passwordVisible) "隐藏密码" else "显示密码", + tint = Amll.colors.ink3, + modifier = Modifier + .size(18.dp) + .clickable(onClick = viewModel::togglePasswordVisible), + ) }, - modifier = Modifier.fillMaxWidth(), ) } } @Composable -private fun CodeForm(state: LoginUiState, viewModel: LoginViewModel) { - Column(modifier = Modifier.fillMaxWidth()) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - SegmentedButton( - selected = state.codeType == CodeType.PHONE, - onClick = { viewModel.switchCodeType(CodeType.PHONE) }, - shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), - ) { Text("手机号") } - SegmentedButton( - selected = state.codeType == CodeType.EMAIL, - onClick = { viewModel.switchCodeType(CodeType.EMAIL) }, - shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), - ) { Text("邮箱") } - } - - Spacer(Modifier.height(12.dp)) +private fun CodeForm( + state: LoginUiState, + viewModel: LoginViewModel, + onSubmit: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + SegmentedTabs( + labels = listOf("手机号", "邮箱"), + selectedIndex = if (state.codeType == CodeType.PHONE) 0 else 1, + onSelect = { index -> + viewModel.switchCodeType(if (index == 0) CodeType.PHONE else CodeType.EMAIL) + }, + ) - OutlinedTextField( + AmllTextField( value = state.dest, onValueChange = viewModel::onDestChange, - label = { Text(if (state.codeType == CodeType.PHONE) "手机号" else "邮箱") }, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = if (state.codeType == CodeType.PHONE) { - KeyboardType.Phone - } else { - KeyboardType.Email - }, - imeAction = ImeAction.Next, - ), - modifier = Modifier.fillMaxWidth(), + label = if (state.codeType == CodeType.PHONE) "手机号" else "邮箱", + placeholder = if (state.codeType == CodeType.PHONE) "请输入手机号" else "请输入邮箱", + keyboardType = if (state.codeType == CodeType.PHONE) KeyboardType.Phone else KeyboardType.Email, + imeAction = ImeAction.Next, ) - Spacer(Modifier.height(12.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - OutlinedTextField( + Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AmllTextField( value = state.code, onValueChange = viewModel::onCodeChange, - label = { Text("验证码") }, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), + label = "验证码", + placeholder = "6 位验证码", + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, modifier = Modifier.weight(1f), + onImeAction = onSubmit, ) - Spacer(Modifier.width(4.dp)) - TextButton( - onClick = viewModel::sendCode, - enabled = state.dest.isNotBlank() && - state.countdown == 0 && - !state.sendingCode, + Box( + Modifier + .height(56.dp) + .width(112.dp) + .clip(RoundedCornerShape(Radius.chip)) + .background(if (state.canSendCode) Amll.colors.primarySoft else Amll.colors.surface2) + .clickable(enabled = state.canSendCode, onClick = viewModel::sendCode), + contentAlignment = Alignment.Center, ) { Text( - when { + text = when { + state.preparingCode -> "检查中…" state.sendingCode -> "发送中…" - state.countdown > 0 -> "${state.countdown}s" + state.countdown > 0 -> "${state.countdown}s 后重发" else -> "获取验证码" - } + }, + style = Font.caption.copy(fontWeight = FontWeight.Medium), + color = if (state.canSendCode) Amll.colors.primary else Amll.colors.ink3, ) } } + + Text( + "验证码由认证服务发送;未注册的账号会直接提示,不会发码", + style = Font.micro, + color = Amll.colors.ink3, + ) } } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginViewModel.kt index 7c2e6e9..c3b18c6 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginViewModel.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LoginViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.amll.hub.android.data.remote.ApiException import dev.amll.hub.android.data.remote.AuthRepository +import dev.amll.hub.android.data.remote.CaptchaConfig import dev.amll.hub.android.data.remote.LoginResult import dev.amll.hub.android.data.remote.UnauthorizedException import kotlinx.coroutines.Job @@ -32,16 +33,29 @@ data class LoginUiState( val code: String = "", val passwordVisible: Boolean = false, val submitting: Boolean = false, + /** check-user 与拉取人机验证配置的短暂过程 */ + val preparingCode: Boolean = false, val sendingCode: Boolean = false, /** 验证码发送成功后的倒计时秒数,0 表示未在倒计时 */ val countdown: Int = 0, val error: String? = null, + /** 非空表示需要人机验证,值为阿里云所需配置 */ + val captcha: CaptchaConfig? = null, + /** null = 等待验证;true = 业务成功;false = 业务失败(SDK 会自动重置) */ + val captchaVerified: Boolean? = null, ) { val canSubmit: Boolean - get() = !submitting && when (tab) { - LoginTab.PASSWORD -> username.isNotBlank() && password.isNotBlank() - LoginTab.CODE -> dest.isNotBlank() && code.isNotBlank() + get() = when (tab) { + LoginTab.PASSWORD -> !submitting && username.isNotBlank() && password.isNotBlank() + LoginTab.CODE -> !submitting && dest.isNotBlank() && code.isNotBlank() } + + val canSendCode: Boolean + get() = tab == LoginTab.CODE && + dest.isNotBlank() && + countdown == 0 && + !sendingCode && + !preparingCode } @HiltViewModel @@ -53,13 +67,20 @@ class LoginViewModel @Inject constructor( val uiState: StateFlow = _uiState.asStateFlow() private var countdownJob: Job? = null + private var captchaConfig: CaptchaConfig? = null - fun switchTab(tab: LoginTab) = _uiState.update { - it.copy(tab = tab, error = null, countdown = 0) + fun switchTab(tab: LoginTab) { + countdownJob?.cancel() + _uiState.update { + it.copy(tab = tab, error = null, countdown = 0, captcha = null, captchaVerified = null) + } } - fun switchCodeType(type: CodeType) = _uiState.update { - it.copy(codeType = type, error = null, dest = "", countdown = 0) + fun switchCodeType(type: CodeType) { + countdownJob?.cancel() + _uiState.update { + it.copy(codeType = type, error = null, dest = "", countdown = 0, captcha = null, captchaVerified = null) + } } fun onUsernameChange(v: String) = _uiState.update { it.copy(username = v, error = null) } @@ -72,23 +93,85 @@ class LoginViewModel @Inject constructor( fun dismissError() = _uiState.update { it.copy(error = null) } + fun dismissCaptcha() = _uiState.update { it.copy(captcha = null, captchaVerified = null) } + + /** + * 与网页端一致的三步: + * 1. check-user 确认账号存在(能给出「未注册」而不是笼统失败) + * 2. 拉取人机验证配置;需要验证就先弹阿里云滑块 + * 3. 拿到 token 后带 captchaType / captchaToken 发码 + */ fun sendCode() { val state = _uiState.value - if (state.dest.isBlank() || state.sendingCode || state.countdown > 0) return + if (!state.canSendCode) return - _uiState.update { it.copy(sendingCode = true, error = null) } + _uiState.update { it.copy(preparingCode = true, error = null, captchaVerified = null) } viewModelScope.launch { - runCatching { repo.sendCode(state.codeType.apiValue, state.dest) } - .onSuccess { startCountdown(60) } - .onFailure { e -> - _uiState.update { it.copy(sendingCode = false, error = e.userMessage()) } + try { + repo.checkUser(state.codeType.apiValue, state.dest) + val config = repo.captchaConfig() + captchaConfig = config + if (config.required) { + _uiState.update { it.copy(preparingCode = false, captcha = config) } + } else { + _uiState.update { it.copy(preparingCode = false) } + sendCodeNow(config, "") } + } catch (e: Throwable) { + _uiState.update { it.copy(preparingCode = false, error = e.userMessage()) } + } } } + /** 人机验证回调:拿到阿里云 verifyParam 后真正发码 */ + fun onCaptchaToken(token: String) { + val config = captchaConfig ?: return + if (token.isBlank()) return + _uiState.update { it.copy(captchaVerified = null, error = null) } + viewModelScope.launch { sendCodeNow(config, token) } + } + + private suspend fun sendCodeNow(config: CaptchaConfig, captchaToken: String) { + val state = _uiState.value + _uiState.update { it.copy(sendingCode = true, error = null) } + runCatching { + repo.sendCode( + checkType = state.codeType.apiValue, + dest = state.dest, + captchaType = if (captchaToken.isBlank()) "" else config.type, + captchaToken = captchaToken, + ) + } + .onSuccess { + val inCaptcha = _uiState.value.captcha != null + _uiState.update { + it.copy( + sendingCode = false, + captchaVerified = if (inCaptcha) true else it.captchaVerified, + ) + } + startCountdown(60) + if (inCaptcha) { + // 留一点时间让 SDK 播完「验证成功」再关闭弹层 + delay(600) + _uiState.update { it.copy(captcha = null, captchaVerified = null) } + } + } + .onFailure { e -> + val inCaptcha = _uiState.value.captcha != null + _uiState.update { + it.copy( + sendingCode = false, + error = e.userMessage(), + captchaVerified = if (inCaptcha) false else it.captchaVerified, + ) + } + } + } + private fun startCountdown(seconds: Int) { countdownJob?.cancel() - _uiState.update { it.copy(sendingCode = false, countdown = seconds) } + _uiState.update { it.copy(countdown = seconds) } countdownJob = viewModelScope.launch { for (i in seconds - 1 downTo 0) { delay(1000) diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricScreen.kt new file mode 100644 index 0000000..83a0825 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricScreen.kt @@ -0,0 +1,89 @@ +package dev.amll.hub.android.ui.screen + +import android.app.Activity +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllLyricView +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.theme.Amll + +/** + * 歌词页:与网页端 LyricsPage 一致 —— 整屏只有一个 AMLL 播放器, + * 歌曲信息、封面、底部控制条、右键菜单都由 AMLL 页面自己绘制。 + */ +@Composable +fun LyricScreen(lyricVm: LyricViewModel, playerVm: PlaybackViewModel, nav: AppNav) { + val lyric by lyricVm.lyric.collectAsStateWithLifecycle() + val player by playerVm.playerState.collectAsStateWithLifecycle() + + val duration = player.durationMs.takeIf { it > 0 } ?: 0L + + if (lyric.songId.isBlank()) { + // 没有网易云 ID,解析不了也播不了,这里才需要提示 + Box(Modifier.fillMaxSize().background(Amll.colors.canvas).statusBarsPadding()) { + EmptyState( + icon = Icons.Outlined.Inbox, + title = lyric.error ?: "无法播放", + description = "这首歌没有关联的网易云 ID", + modifier = Modifier.align(Alignment.Center), + ) + } + return + } + + // 歌词页永远是深色底:状态栏/导航栏图标固定用浅色, + // 系统处于浅色模式时主题会把它设成深色图标,在这里覆盖回来,退出时还原。 + val view = LocalView.current + val darkTheme = isSystemInDarkTheme() + DisposableEffect(view) { + val window = (view.context as Activity).window + val controller = WindowCompat.getInsetsController(window, view) + controller.isAppearanceLightStatusBars = false + controller.isAppearanceLightNavigationBars = false + onDispose { + controller.isAppearanceLightStatusBars = !darkTheme + controller.isAppearanceLightNavigationBars = !darkTheme + } + } + + // 与 AMLL 页面自身的 dark 底色一致,加载中不会闪一下纯黑。 + // 不避让状态栏:WebView 铺满整屏,状态栏高度由原生通过 setInsets 交给页面内缩, + // 否则状态栏区域会露出这层近黑色底,看起来像一条黑带。 + Box(Modifier.fillMaxSize().background(Color(0xFF0B0B0F))) { + AmllLyricView( + rawLyric = lyric.source.text, + lyricFormat = lyric.source.format, + lyricTranslation = lyric.source.translation, + positionMs = player.positionMs, + playing = player.playing, + title = lyric.title, + artist = lyric.artist, + durationMs = duration, + cover = lyric.cover, + dark = true, + onTogglePlay = playerVm::togglePlay, + onSeek = playerVm::seekTo, + onClose = nav.back, + onNext = playerVm::next, + onPrev = playerVm::prev, + onToggleShuffle = playerVm::toggleShuffle, + onCycleRepeat = playerVm::cycleRepeat, + modifier = Modifier.fillMaxSize(), + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitScreen.kt new file mode 100644 index 0000000..5f9c27a --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitScreen.kt @@ -0,0 +1,398 @@ +package dev.amll.hub.android.ui.screen + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CloudUpload +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.AmllTextField +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.FieldRow +import dev.amll.hub.android.ui.component.GhostButton +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.ChipShape +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import kotlinx.coroutines.delay + +/* ============================ 歌词投稿 ============================ */ + +private val LANG_OPTIONS = listOf( + "zh" to "汉语", + "en" to "英语", + "ja" to "日语", + "ko" to "韩语", + "others" to "其他", +) + +@Composable +fun LyricSubmitScreen( + nav: AppNav, + vm: LyricSubmitViewModel = hiltViewModel(), +) { + val ui by vm.state.collectAsStateWithLifecycle() + + val ttmlPicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + uri?.let(vm::pickTtml) + } + val audioPicker = + rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> + vm.addAudio(uris) + } + + // 投稿成功后返回创作中心列表(音源失败时多停留一会儿让用户看到提示) + LaunchedEffect(ui.successId) { + if (ui.successId == null) return@LaunchedEffect + delay(if (ui.messageIsError) 3000 else 800) + nav.back() + } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding(), + ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AmllTopBar("歌词投稿", onBack = nav.back) + + // 文件选择 + SectionHeader("歌词文件 (TTML)") + AmllCard( + padding = 16.dp, + modifier = Modifier.clickable { ttmlPicker.launch(arrayOf("*/*")) }, + ) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + Icons.Rounded.CloudUpload, + null, + tint = Amll.colors.ink3, + modifier = Modifier.width(26.dp), + ) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + ui.fileName.ifBlank { "点击选择 .ttml 文件" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + when { + ui.validating -> "正在校验文件…" + ui.fileName.isNotBlank() -> "点击可重新选择" + else -> "文件会先经过后端校验,再自动重排" + }, + style = Font.micro, + color = Amll.colors.ink3, + ) + } + if (ui.fileName.isNotBlank() && !ui.validating) { + Text( + "移除", + style = Font.micro, + color = Amll.colors.warning, + modifier = Modifier.clickable { vm.resetFile() }, + ) + } + } + } + + val validation = ui.validation + when { + ui.validateError != null -> Text( + ui.validateError.orEmpty(), + style = Font.caption, + color = Amll.colors.error, + ) + validation != null && !validation.valid -> { + AmllCard(padding = 16.dp) { + Text("校验未通过", style = Font.cardTitle, color = Amll.colors.error) + if (validation.parseError.isNotBlank()) { + Text(validation.parseError, style = Font.caption, color = Amll.colors.ink2) + } + validation.errors.take(6).forEach { + Text("· $it", style = Font.caption, color = Amll.colors.ink2) + } + } + } + } + + if (validation != null) { + SectionHeader("文件信息") + AmllCard(padding = 16.dp) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + FieldRow("标题", validation.titles.joinToString("、").ifBlank { "—" }) + FieldRow("语言", validation.language.ifBlank { "—" }) + FieldRow("校验结果", if (validation.valid) "通过" else "未通过") + } + } + } + + // 语言 + SectionHeader("语言") + AmllCard(padding = 16.dp) { + Row( + Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + LANG_OPTIONS.forEach { (code, label) -> + FilterChip( + selected = code in ui.languages, + onClick = { vm.toggleLanguage(code) }, + label = { + Text(label, style = Font.micro.copy(fontWeight = FontWeight.Medium)) + }, + shape = ChipShape, + colors = FilterChipDefaults.filterChipColors( + containerColor = Amll.colors.surface2, + labelColor = Amll.colors.ink2, + selectedContainerColor = Amll.colors.primary, + selectedLabelColor = Amll.colors.onPrimary, + ), + border = null, + ) + } + } + if ("others" in ui.languages) { + AmllTextField( + value = ui.otherLangInput, + onValueChange = vm::setOtherLangInput, + placeholder = "输入语言名后点添加", + imeAction = androidx.compose.ui.text.input.ImeAction.Done, + onImeAction = vm::addOtherLanguage, + modifier = Modifier.padding(top = 10.dp), + ) + ui.otherLanguages.forEach { value -> + TagPill(value, onRemove = { vm.removeOtherLanguage(value) }) + } + } + val count = ui.languagesText.size + Text( + "已选 $count / ${LyricSubmitViewModel.MAX_LANGUAGES}", + style = Font.micro, + color = Amll.colors.ink3, + modifier = Modifier.padding(top = 8.dp), + ) + } + + // 标签 + SectionHeader("标签") + AmllCard(padding = 16.dp) { + AmllTextField( + value = ui.tagInput, + onValueChange = vm::setTagInput, + placeholder = "输入标签后回车添加", + imeAction = androidx.compose.ui.text.input.ImeAction.Done, + onImeAction = vm::addTag, + ) + ui.tags.forEach { value -> + TagPill(value, onRemove = { vm.removeTag(value) }) + } + } + + // 备注 + SectionHeader("备注") + MultilineField( + value = ui.notes, + onValueChange = vm::setNotes, + placeholder = "给审核员的补充说明(可选)", + minLines = 3, + ) + + // 音源附件 + SectionHeader("音源附件(可选)") + AmllCard(padding = 16.dp) { + Row( + Modifier + .fillMaxWidth() + .height(44.dp) + .clip(RoundedCornerShape(Radius.field)) + .background(Amll.colors.surface2) + .clickable { audioPicker.launch(arrayOf("audio/*")) } + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Icon( + Icons.Rounded.MusicNote, + null, + tint = Amll.colors.ink3, + modifier = Modifier.width(18.dp), + ) + Text( + if (ui.audioFiles.isEmpty()) "添加音源文件(可多选)" else "继续添加", + style = Font.bodySmall, + color = Amll.colors.ink2, + modifier = Modifier.weight(1f), + ) + } + ui.audioFiles.forEachIndexed { index, audio -> + Row( + Modifier.fillMaxWidth().padding(top = if (index == 0) 10.dp else 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + audio.name, + style = Font.caption, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Text( + "移除", + style = Font.micro, + color = Amll.colors.warning, + modifier = Modifier.clickable { vm.removeAudio(audio.uri) }, + ) + } + } + } + + // 原创声明(提交审核必选,存草稿不受限) + Row( + Modifier.fillMaxWidth().clickable { vm.setAgreed(!ui.agreed) }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = ui.agreed, + onCheckedChange = vm::setAgreed, + colors = CheckboxDefaults.colors(checkedColor = Amll.colors.primary), + ) + Text( + "我确认该歌词为原创或已获授权,并遵守投稿规范", + style = Font.caption, + color = Amll.colors.ink2, + ) + } + + ui.message?.let { + Text( + it, + style = Font.caption, + color = if (ui.messageIsError) Amll.colors.error else Amll.colors.success, + ) + } + if (ui.audioProgress.isNotBlank()) { + Text(ui.audioProgress, style = Font.micro, color = Amll.colors.ink3) + } + + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + GhostButton( + text = if (ui.submitting) "处理中…" else "存草稿", + onClick = { vm.submit(draft = true) }, + modifier = Modifier.weight(1f), + ) + PrimaryButton( + text = if (ui.submitting) "提交中…" else "立即投稿", + onClick = { vm.submit(draft = false) }, + modifier = Modifier.weight(1f), + enabled = ui.canSubmit && ui.agreed, + ) + } + + Text( + "上传即表示同意《AMLL Hub 使用协议》与《社区公约》", + style = Font.micro, + color = Amll.colors.ink3, + ) + } + } +} + +@Composable +private fun TagPill(label: String, onRemove: () -> Unit) { + Box( + Modifier + .padding(top = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(999.dp)) + .border(1.dp, Amll.colors.border, RoundedCornerShape(999.dp)) + .clickable(onClick = onRemove) + .padding(horizontal = 10.dp, vertical = 5.dp), + ) { + Text("$label ×", style = Font.micro, color = Amll.colors.ink2) + } +} + +@Composable +fun MultilineField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + minLines: Int = 4, + maxLength: Int? = null, +) { + OutlinedTextField( + value = value, + onValueChange = { next -> + onValueChange(if (maxLength != null) next.take(maxLength) else next) + }, + placeholder = { Text(placeholder, style = Font.bodySmall, color = Amll.colors.ink3) }, + minLines = minLines, + textStyle = Font.body, + shape = RoundedCornerShape(Radius.field), + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = Amll.colors.surface2, + unfocusedContainerColor = Amll.colors.surface2, + focusedTextColor = Amll.colors.ink, + unfocusedTextColor = Amll.colors.ink, + cursorColor = Amll.colors.primary, + focusedBorderColor = Amll.colors.primary, + unfocusedBorderColor = Amll.colors.border, + ), + ) +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitViewModel.kt new file mode 100644 index 0000000..31b3953 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricSubmitViewModel.kt @@ -0,0 +1,328 @@ +package dev.amll.hub.android.ui.screen + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.amll.hub.android.data.remote.CreateSubmissionRequest +import dev.amll.hub.android.data.remote.TtmlValidationDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import java.util.Locale +import javax.inject.Inject + +/** 本地选中的文件(TTML / 音源) */ +data class PickedFile( + val uri: Uri, + val name: String, + val size: Long = 0, + val mime: String = "", +) + +/** + * 歌词投稿:对齐网页端 LyricSubmitForm 的核心流程 + * (TTML 选择 → /validate 校验 → 语言/标签/备注 → 上传 TTML → 创建投稿 → 上传音源)。 + */ +@HiltViewModel +class LyricSubmitViewModel @Inject constructor( + private val repo: ContentRepository, + @ApplicationContext private val context: Context, +) : ViewModel() { + + data class UiState( + val fileName: String = "", + val validating: Boolean = false, + val validation: TtmlValidationDto? = null, + val validateError: String? = null, + val notes: String = "", + val languages: List = emptyList(), + val otherLanguages: List = emptyList(), + val otherLangInput: String = "", + val tags: List = emptyList(), + val tagInput: String = "", + val agreed: Boolean = false, + val audioFiles: List = emptyList(), + val submitting: Boolean = false, + val audioProgress: String = "", + val message: String? = null, + val messageIsError: Boolean = false, + val successId: Long? = null, + ) { + val languagesText: List get() = languages + otherLanguages + val canSubmit: Boolean + get() = fileName.isNotBlank() && validation?.valid == true && !submitting + } + + private val _state = MutableStateFlow(UiState()) + val state: StateFlow = _state.asStateFlow() + + private var ttmlUri: Uri? = null + + fun setNotes(value: String) = _state.update { it.copy(notes = value) } + + fun setTagInput(value: String) = _state.update { it.copy(tagInput = value) } + + fun setOtherLangInput(value: String) = _state.update { it.copy(otherLangInput = value) } + + fun setAgreed(value: Boolean) = _state.update { it.copy(agreed = value) } + + fun dismissMessage() = _state.update { it.copy(message = null) } + + fun pickTtml(uri: Uri) { + ttmlUri = uri + val name = displayName(uri) + _state.update { + it.copy( + fileName = name, + validating = true, + validation = null, + validateError = null, + message = null, + ) + } + viewModelScope.launch { + val text = withContext(Dispatchers.IO) { + runCatching { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } } + .getOrNull() + } + if (text == null) { + _state.update { it.copy(validating = false, validateError = "读取文件失败") } + return@launch + } + val content = text.toString(Charsets.UTF_8) + repo.validateTtml(content) + .onSuccess { result -> + val autoLanguage = languageCode(result.language) + _state.update { current -> + current.copy( + validating = false, + validation = result, + validateError = null, + // 与网页端一致:自动识别语言,但只补第一次,不覆盖已选 + languages = if (current.languages.isEmpty() && autoLanguage != null) { + listOf(autoLanguage) + } else { + current.languages + }, + ) + } + } + .onFailure { e -> + _state.update { + it.copy(validating = false, validateError = e.message ?: "校验失败") + } + } + } + } + + fun toggleLanguage(code: String) { + _state.update { current -> + val exists = code in current.languages + when { + exists -> current.copy( + languages = current.languages - code, + // 取消「其他」时连带清掉自定义语言 + otherLanguages = if (code == "others") emptyList() else current.otherLanguages, + ) + current.languagesText.size >= MAX_LANGUAGES -> current + else -> current.copy(languages = current.languages + code) + } + } + } + + fun addOtherLanguage() { + _state.update { current -> + val value = current.otherLangInput.trim() + when { + value.isEmpty() -> current + value in current.otherLanguages -> current.copy(otherLangInput = "") + current.languagesText.size >= MAX_LANGUAGES -> current + else -> current.copy( + otherLanguages = current.otherLanguages + value, + otherLangInput = "", + ) + } + } + } + + fun removeOtherLanguage(value: String) { + _state.update { it.copy(otherLanguages = it.otherLanguages - value) } + } + + fun addTag() { + _state.update { current -> + val value = current.tagInput.trim() + when { + value.isEmpty() -> current + value in current.tags -> current.copy(tagInput = "") + else -> current.copy(tags = current.tags + value, tagInput = "") + } + } + } + + fun removeTag(value: String) { + _state.update { it.copy(tags = it.tags - value) } + } + + fun addAudio(uris: List) { + if (uris.isEmpty()) return + val picked = uris.map { uri -> + PickedFile( + uri = uri, + name = displayName(uri), + size = fileSize(uri), + mime = context.contentResolver.getType(uri).orEmpty(), + ) + } + _state.update { it.copy(audioFiles = it.audioFiles + picked, message = null) } + } + + fun removeAudio(uri: Uri) { + _state.update { it.copy(audioFiles = it.audioFiles.filterNot { item -> item.uri == uri }) } + } + + fun resetFile() { + ttmlUri = null + _state.update { + it.copy( + fileName = "", + validation = null, + validateError = null, + validating = false, + ) + } + } + + /** draft=true 存草稿,否则提交审核 */ + fun submit(draft: Boolean) { + val current = _state.value + if (current.submitting) return + val uri = ttmlUri ?: return + if (current.validation?.valid != true) return + if (!draft && !current.agreed) return + + viewModelScope.launch { + _state.update { it.copy(submitting = true, message = null, audioProgress = "") } + val bytes = withContext(Dispatchers.IO) { + runCatching { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } } + .getOrNull() + } + if (bytes == null) { + _state.update { it.copy(submitting = false, message = "读取文件失败", messageIsError = true) } + return@launch + } + + val upload = repo.uploadTtml(current.fileName, bytes) + val uploadedName = upload.getOrElse { e -> + val failure = e.message ?: "文件上传失败" + _state.update { + it.copy(submitting = false, message = failure, messageIsError = true) + } + return@launch + } + + val validation = current.validation + val title = validation?.titles?.firstOrNull()?.takeIf { it.isNotBlank() } + ?: current.fileName.removeSuffix(".ttml").ifBlank { "未命名稿件" } + val languages = current.languagesText + val request = CreateSubmissionRequest( + title = title, + metadata = validation?.metadata ?: JsonObject(emptyMap()), + fileName = uploadedName, + notes = current.notes.trim().takeIf { it.isNotEmpty() }, + tags = current.tags.takeIf { it.isNotEmpty() }, + language = languages.firstOrNull(), + languages = languages.takeIf { it.isNotEmpty() }, + status = if (draft) "draft" else "pending", + ) + val created = repo.createSubmission(request).getOrElse { e -> + _state.update { + it.copy(submitting = false, message = e.message ?: "投稿失败", messageIsError = true) + } + return@launch + } + + // 音源为可选附件:单个失败不影响投稿本身,与网页端一致 + val failures = mutableListOf() + current.audioFiles.forEachIndexed { index, audio -> + _state.update { + it.copy(audioProgress = "正在上传音源 ${index + 1}/${current.audioFiles.size}") + } + val audioBytes = withContext(Dispatchers.IO) { + runCatching { + context.contentResolver.openInputStream(audio.uri)?.use { input -> input.readBytes() } + }.getOrNull() + } + val mime = audio.mime.takeIf { it.isNotBlank() } ?: "audio/mpeg" + if (audioBytes == null || + repo.uploadAudio(created, audio.name, audioBytes, mime).isFailure + ) { + failures += audio.name + } + } + + val message = when { + failures.isNotEmpty() -> + "${if (draft) "草稿已保存" else "投稿已创建"},但 ${failures.size} 个音源上传失败" + + "(${failures.joinToString("、")}),可在详情页补传" + draft -> "草稿已保存" + else -> "投稿成功" + } + _state.update { + it.copy( + submitting = false, + audioProgress = "", + message = message, + messageIsError = failures.isNotEmpty(), + successId = created, + ) + } + } + } + + private fun displayName(uri: Uri): String { + val fromResolver = runCatching { + context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) + ?.use { cursor -> + if (cursor.moveToFirst()) cursor.getString(0) else null + } + }.getOrNull() + return fromResolver?.takeIf { it.isNotBlank() } + ?: uri.lastPathSegment?.substringAfterLast('/')?.takeIf { it.isNotBlank() } + ?: "文件" + } + + private fun fileSize(uri: Uri): Long = runCatching { + context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null) + ?.use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else 0L } + ?: 0L + }.getOrDefault(0L) + + companion object { + /** 与后端 model.MaxLanguages 一致 */ + const val MAX_LANGUAGES = 10 + + /** TTML 元数据语言码 → 后端枚举;识别不了就交给用户手选 */ + fun languageCode(raw: String): String? { + val value = raw.trim().lowercase(Locale.ROOT) + if (value.isEmpty()) return null + return when { + value.startsWith("zh") || value == "cn" || value == "chs" || value == "zh_cn" -> "zh" + value.startsWith("en") -> "en" + value.startsWith("ja") || value == "jp" -> "ja" + value.startsWith("ko") || value == "kr" -> "ko" + else -> null + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricViewModel.kt new file mode 100644 index 0000000..75ad718 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/LyricViewModel.kt @@ -0,0 +1,136 @@ +package dev.amll.hub.android.ui.screen + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.repo.LyricRepository +import dev.amll.hub.android.data.repo.LyricSource +import dev.amll.hub.android.data.repo.PlaybackRepository +import dev.amll.hub.android.playback.PlayerController +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val TAG = "LyricViewModel" + +data class LyricUi( + val loading: Boolean = false, + val songId: String = "", + val title: String = "", + val artist: String = "", + val cover: String? = null, + /** 原始歌词文本,解析交给歌词页里的 AMLL 官方解析器 */ + val source: LyricSource = LyricSource(), + val error: String? = null, +) + +/** 打开一首歌:解析音源 + 按优先级找歌词,然后把曲目交给播放器 */ +@HiltViewModel +class LyricViewModel @Inject constructor( + private val playbackRepo: PlaybackRepository, + private val lyricRepo: LyricRepository, + private val player: PlayerController, +) : ViewModel() { + + val lyric = MutableStateFlow(LyricUi()) + + fun openTrack( + songId: String, + title: String, + artist: String, + cover: String?, + rawLyricFile: String? = null, + /** 从播放条进歌词页时为 false:只补歌词,不打断当前播放 */ + startPlayback: Boolean = true, + ) { + val id = songId.trim() + if (id.isBlank()) { + lyric.value = LyricUi( + loading = false, + title = title, + artist = artist, + cover = cover, + error = "该歌曲没有关联的网易云 ID", + ) + return + } + // 同一首歌已经加载过就不再重复请求 + if (lyric.value.songId == id && + lyric.value.source.text.isNotEmpty() && + player.state.value.track?.songId == id + ) { + return + } + + lyric.value = LyricUi(loading = true, songId = id, title = title, artist = artist, cover = cover) + viewModelScope.launch { + val quality = player.state.value.quality + val resolved = playbackRepo.resolve(id, quality.key) + val music = resolved.music + val parseError = resolved.error + + // 请求必须兜住:viewModelScope 里漏出去的异常会直接崩掉进程 + val source = runCatching { + lyricRepo.load(id, rawLyricFile, music?.lyric.orEmpty(), music?.tlyric.orEmpty()) + }.getOrElse { + Log.e(TAG, "openTrack id=$id lyric load failed", it) + LyricSource() + } + Log.d( + TAG, + "openTrack id=$id raw=${source.text.length}B format=${source.format} " + + "translation=${source.translation.length}B " + + "ncmLyric=${music?.lyric?.length ?: 0}B url=${!music?.displayUrl.isNullOrBlank()} " + + "parseError=$parseError", + ) + + val resolvedTitle = music?.displayName?.takeIf { it.isNotBlank() } ?: title + val resolvedArtist = music?.displayArtist?.takeIf { it.isNotBlank() } ?: artist + val resolvedCover = music?.displayCover?.takeIf { it.isNotBlank() } ?: cover + + lyric.update { + it.copy( + loading = false, + songId = id, + title = resolvedTitle, + artist = resolvedArtist, + cover = resolvedCover, + source = source, + error = null, + ) + } + val url = music?.displayUrl?.takeIf { it.isNotBlank() } + if (url == null) { + // 播放条只显示默认的「暂无音源」,具体失败原因留给日志排查 + Log.w(TAG, "openTrack id=$id no playable url: $parseError") + } + // 从播放条进入歌词页时当前曲目已经在播,不能重新解析后从头播放, + // 否则队列位置和播放进度都会被重置 + if (startPlayback) { + player.playResolved( + PlayerController.Track( + songId = id, + title = resolvedTitle, + artist = resolvedArtist, + coverUrl = resolvedCover, + streamUrl = url, + albumName = music?.albumName.orEmpty(), + lyricLrc = if (source.format == "lrc") source.text else "", + lyricTranslation = source.translation, + quality = quality, + actualQuality = resolved.actual, + ), + ) + } + } + } + + /** 从播放条进歌词页前调用:当前曲目的歌词还没加载就补一次 */ + fun ensurePlayerLyric() { + val track = player.state.value.track ?: return + if (lyric.value.songId == track.songId && lyric.value.source.text.isNotEmpty()) return + openTrack(track.songId, track.title, track.artist, track.coverUrl, startPlayback = false) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesScreen.kt new file mode 100644 index 0000000..f3203ee --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesScreen.kt @@ -0,0 +1,166 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.TabScaffold +import dev.amll.hub.android.ui.bottomBarPadding +import dev.amll.hub.android.ui.component.ChipTone +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.PageHeader +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font + +/* ============================ 10 消息 ============================ */ + +@Composable +fun MessagesScreen(vm: MessagesViewModel, nav: AppNav) { + LaunchedEffect(Unit) { if (vm.messages.value.items.isEmpty()) vm.loadMessages() } + val ui by vm.messages.collectAsStateWithLifecycle() + var tab by remember { mutableIntStateOf(0) } + val tabs = listOf("全部", "审核", "投稿", "评论", "系统") + + TabScaffold { + Column( + Modifier + .fillMaxSize() + .padding(start = 20.dp, end = 20.dp, top = 10.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + PageHeader("消息中心") { + Text( + "全部已读", + style = Font.caption, + color = Amll.colors.ink3, + modifier = Modifier.clickable { vm.markAllRead() }, + ) + } + + Row(Modifier.fillMaxWidth().height(34.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { + tabs.forEachIndexed { index, label -> + val active = index == tab + Box( + Modifier + .weight(1f) + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)) + .background(if (active) Amll.colors.surface else Color.Transparent) + .clickable { tab = index }, + contentAlignment = Alignment.Center, + ) { + Text( + label, + style = Font.caption.copy(fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium), + color = if (active) Amll.colors.ink else Amll.colors.ink3, + ) + } + } + } + + val filtered = remember(ui.items, tab) { + val key = when (tab) { + 1 -> "review" + 2 -> "submission" + 3 -> "comment" + 4 -> "system" + else -> null + } + if (key == null) ui.items else ui.items.filter { it.type == key } + } + + when { + ui.loading -> SkeletonList(5) + filtered.isEmpty() -> EmptyState(Icons.Outlined.Inbox, "暂无消息", "有新的审核结果会在这里通知你") + else -> LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + contentPadding = PaddingValues(bottom = bottomBarPadding(8.dp)), + ) { + itemsIndexed(filtered) { _, message -> + MessageRow(message) { vm.markRead(message.id) } + } + } + } + } + } +} + +@Composable +private fun MessageRow( + message: dev.amll.hub.android.data.remote.MessageDto, + onRead: () -> Unit, +) { + val tone = when (message.type) { + "review" -> ChipTone.SUCCESS + "submission" -> ChipTone.WARNING + "comment" -> ChipTone.PRIMARY + else -> ChipTone.INFO + } + Row( + Modifier.fillMaxWidth().height(64.dp).clickable(onClick = onRead), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + Modifier + .size(36.dp) + .clip(RoundedCornerShape(12.dp)) + .background(if (tone == ChipTone.SUCCESS) Amll.colors.successSoft else Amll.colors.surface2), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Outlined.Inbox, null, tint = Amll.colors.ink2, modifier = Modifier.size(18.dp)) + } + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + message.title.ifBlank { "通知" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + message.content, + style = Font.caption, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + message.createdAt.take(10), + style = Font.micro, + color = Amll.colors.ink3, + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesViewModel.kt new file mode 100644 index 0000000..7d6355b --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/MessagesViewModel.kt @@ -0,0 +1,51 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.MessageDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MessagesViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val messages = MutableStateFlow(ListUi()) + + fun loadMessages() { + viewModelScope.launch { + messages.update { it.copy(loading = true, error = null) } + val result = repo.notifications() + val items = result.getOrDefault(emptyList()) + messages.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无消息" else null, + ) + } + } + } + + fun markRead(id: Long) { + viewModelScope.launch { + repo.markRead(id) + messages.update { ui -> + ui.copy(items = ui.items.map { if (it.id == id) it.copy(read = true) else it }) + } + } + } + + fun markAllRead() { + viewModelScope.launch { + repo.markAllRead() + messages.update { ui -> ui.copy(items = ui.items.map { it.copy(read = true) }) } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseScreen.kt new file mode 100644 index 0000000..7e16b57 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseScreen.kt @@ -0,0 +1,634 @@ +package dev.amll.hub.android.ui.screen + +import android.Manifest +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.NcmSongDto +import dev.amll.hub.android.data.remote.NcmTrackDto +import dev.amll.hub.android.download.DownloadStore +import dev.amll.hub.android.download.hasLegacyWritePermission +import dev.amll.hub.android.download.hasNotificationPermission +import dev.amll.hub.android.download.needsLegacyWritePermission +import dev.amll.hub.android.playback.NcmQuality +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.AmllLyricPreview +import dev.amll.hub.android.ui.component.AmllTextField +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.CoverImage +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.GhostButton +import dev.amll.hub.android.ui.component.PlayCircleButton +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.QualityPicker +import dev.amll.hub.android.ui.component.SectionHeader +import dev.amll.hub.android.ui.component.SegmentedTabs +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import java.time.Duration +import java.util.Locale + +private val MODES = listOf("关键词搜索", "单曲 ID", "歌单 ID") + +/** + * 网易云解析:对应网页端的 /ncm 页面。 + * + * 三种入口都是「拿到网易云 ID → 交给播放器解析」,搜索结果 / 歌单曲目 + * 直接在页面内就地播放,不再跳歌词页;音质在顶栏统一切换。 + */ +@Composable +fun NcmParseScreen( + nav: AppNav, + vm: NcmParseViewModel = hiltViewModel(), + playbackVm: PlaybackViewModel = hiltViewModel(), +) { + val ui by vm.state.collectAsStateWithLifecycle() + val playback by playbackVm.playerState.collectAsStateWithLifecycle() + val download by vm.downloadState.collectAsStateWithLifecycle() + val context = LocalContext.current + + var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) } + + // API 28 及以下没写权限就保存不到下载目录,只有授予后才继续 + val storageLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { granted -> + val action = pendingDownload + pendingDownload = null + if (granted) { + action?.invoke() + } else { + vm.notifyError("需要存储权限才能保存到下载目录") + } + } + // 通知权限拒绝不阻塞下载,但两个权限都缺时要接着请求写权限 + val notificationLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { + val action = pendingDownload + pendingDownload = null + if (action != null) { + if (needsLegacyWritePermission() && !hasLegacyWritePermission(context)) { + pendingDownload = action + storageLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + action() + } + } + } + + val runDownload: (() -> Unit) -> Unit = { action -> + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !hasNotificationPermission(context) -> { + pendingDownload = action + notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + needsLegacyWritePermission() && !hasLegacyWritePermission(context) -> { + pendingDownload = action + storageLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + else -> action() + } + } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .imePadding(), + ) { + Column( + Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + AmllTopBar( + "网易云解析", + onBack = nav.back, + trailing = { QualityPicker(ui.quality, vm::setQuality) }, + ) + SegmentedTabs(MODES, ui.mode, vm::setMode) + } + + ui.error?.let { + Text( + it, + style = Font.micro, + color = Amll.colors.warning, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + ) + } + + when (ui.mode) { + 0 -> SearchMode(ui, vm, playback.track?.songId) + 1 -> SongMode(ui, vm, runDownload) + else -> PlaylistMode(ui, vm, download, playback.track?.songId, runDownload) + } + } +} + +/* ------------------------------ 关键词搜索 ------------------------------ */ + +@Composable +private fun SearchMode( + ui: NcmParseViewModel.UiState, + vm: NcmParseViewModel, + currentId: String?, +) { + Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 10.dp)) { + AmllTextField( + value = ui.query, + onValueChange = vm::setQuery, + placeholder = "歌名 / 歌手,回车搜索", + imeAction = ImeAction.Search, + onImeAction = vm::search, + ) + Spacer(Modifier.height(10.dp)) + PrimaryButton( + text = if (ui.searching) "搜索中…" else "搜索", + onClick = vm::search, + enabled = ui.query.isNotBlank() && !ui.searching, + ) + } + + when { + ui.searching -> Column(Modifier.padding(horizontal = 20.dp)) { SkeletonList(5) } + ui.results.isEmpty() -> EmptyState( + icon = Icons.Outlined.Inbox, + title = "搜网易云曲库", + description = "搜到结果点一下就能直接播", + ) + else -> LazyColumn( + contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + item { SectionHeader("共 ${ui.results.size} 首") } + items(ui.results) { song -> + NcmSongRow( + song = song, + current = song.displayId.isNotBlank() && song.displayId == currentId, + onClick = { vm.playSearch(song) }, + ) + } + } + } +} + +@Composable +private fun NcmSongRow(song: NcmSongDto, current: Boolean, onClick: () -> Unit) { + val subtitle = song.displayArtist + + song.displayAlbum.takeIf { it.isNotBlank() }?.let { " · $it" }.orEmpty() + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.field)) + .background(if (current) Amll.colors.primarySoft else Color.Transparent) + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoverImage(song.displayCover.ifBlank { null }, song.displayName, 44.dp, radius = Radius.field) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + song.displayName, + style = Font.bodyMedium, + color = if (current) Amll.colors.primary else Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + subtitle, + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (song.displayDuration > 0) { + Text(formatDuration(song.displayDuration), style = Font.micro, color = Amll.colors.ink3) + } + PlayCircleButton(size = 26.dp) + } +} + +/* ------------------------------ 单曲解析 ------------------------------ */ + +@Composable +private fun SongMode( + ui: NcmParseViewModel.UiState, + vm: NcmParseViewModel, + runDownload: (() -> Unit) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + AmllTextField( + value = ui.songInput, + onValueChange = vm::setSongInput, + placeholder = "歌曲 ID 或 music.163.com 链接", + label = "支持纯数字 ID、带 id 参数的链接、整段分享文案", + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + onImeAction = vm::parseSong, + ) + PrimaryButton( + text = if (ui.parsing) "解析中…" else "解析", + onClick = vm::parseSong, + enabled = ui.songInput.isNotBlank() && !ui.parsing, + ) + + if (ui.parsing) SkeletonList(2) + + ui.parsed?.let { parsed -> + ParsedCard( + parsed = parsed, + quality = ui.quality, + onPlay = vm::playParsed, + onDownload = { runDownload(vm::downloadParsed) }, + ) + + SectionHeader("歌词预览") + when { + ui.lyricLoading -> SkeletonList(3) + parsed.source.text.isBlank() -> EmptyState( + icon = Icons.Rounded.MusicNote, + title = "暂无歌词", + description = "这首歌没有可用的歌词文本", + ) + else -> AmllLyricPreview( + rawLyric = parsed.source.text, + lyricFormat = parsed.source.format, + lyricTranslation = parsed.source.translation, + title = parsed.music.displayName, + artist = parsed.music.displayArtist, + cover = parsed.music.displayCover.ifBlank { null }, + ) + } + } + } +} + +@Composable +private fun ParsedCard( + parsed: NcmParseViewModel.Parsed, + quality: NcmQuality, + onPlay: () -> Unit, + onDownload: () -> Unit, +) { + val music = parsed.music + val actual = parsed.actual ?: quality + AmllCard { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoverImage( + music.displayCover.ifBlank { null }, + music.displayName, + 64.dp, + radius = Radius.field, + ) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + music.displayName.ifBlank { "未知歌曲" }, + style = Font.cardTitle.copy(fontSize = 15.sp), + color = Amll.colors.ink, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + music.displayArtist.ifBlank { "未知歌手" }, + style = Font.caption, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + music.albumName.ifBlank { "未知专辑" }, + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + Spacer(Modifier.height(10.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + actual.label, + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = Amll.colors.primary, + ) + if (NcmQuality.isDowngraded(parsed.actual, quality)) { + Text( + "已降级", + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = Amll.colors.warning, + ) + } + if (music.displayDuration > 0) { + Text( + formatDuration(music.displayDuration), + style = Font.micro, + color = Amll.colors.ink3, + ) + } + if (music.size.isNotBlank()) { + Text(music.size, style = Font.micro, color = Amll.colors.ink3) + } + Text( + "ID ${parsed.songId}", + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (music.displayUrl.isBlank()) { + Spacer(Modifier.height(4.dp)) + Text( + "无可用播放链接", + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = Amll.colors.warning, + ) + } + + Spacer(Modifier.height(12.dp)) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + PrimaryButton("播放", onClick = onPlay, modifier = Modifier.weight(1f)) + GhostButton("下载", onClick = onDownload, modifier = Modifier.weight(1f)) + } + } +} + +/* ------------------------------ 歌单解析 ------------------------------ */ + +@Composable +private fun PlaylistMode( + ui: NcmParseViewModel.UiState, + vm: NcmParseViewModel, + download: DownloadStore.State, + currentId: String?, + runDownload: (() -> Unit) -> Unit, +) { + Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 10.dp)) { + AmllTextField( + value = ui.playlistInput, + onValueChange = vm::setPlaylistInput, + placeholder = "歌单 ID,例如 2884035", + imeAction = ImeAction.Done, + onImeAction = vm::parsePlaylist, + ) + Spacer(Modifier.height(10.dp)) + PrimaryButton( + text = if (ui.playlistLoading) "解析中…" else "解析歌单", + onClick = vm::parsePlaylist, + enabled = ui.playlistInput.isNotBlank() && !ui.playlistLoading, + ) + } + + val detail = ui.playlist + when { + ui.playlistLoading -> Column(Modifier.padding(horizontal = 20.dp)) { SkeletonList(4) } + detail == null -> EmptyState( + icon = Icons.Outlined.Inbox, + title = "解析网易云歌单", + description = "填歌单 ID,解析出来的曲目点一下就能播", + ) + else -> LazyColumn( + contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + item { + PlaylistHeader( + name = detail.name, + coverUrl = detail.coverImgUrl.ifBlank { null }, + trackCount = detail.tracks.size, + creator = detail.creator?.nickname.orEmpty(), + onPlayAll = vm::playPlaylistAll, + onDownloadAll = { runDownload(vm::downloadPlaylist) }, + ) + } + if (download.running || download.message != null) { + item { DownloadProgressCard(download) } + } + itemsIndexed(detail.tracks) { index, track -> + NcmTrackRow( + track = track, + current = track.displayId.isNotBlank() && track.displayId == currentId, + onClick = { vm.playPlaylistTrack(index) }, + onDownload = { + runDownload { vm.downloadPlaylistTrack(index) } + }, + ) + } + } + } +} + +@Composable +private fun PlaylistHeader( + name: String, + coverUrl: String?, + trackCount: Int, + creator: String, + onPlayAll: () -> Unit, + onDownloadAll: () -> Unit, +) { + AmllCard(modifier = Modifier.padding(bottom = 8.dp), padding = 14.dp) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoverImage(coverUrl, name, 64.dp, radius = Radius.field) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + name.ifBlank { "未命名歌单" }, + style = Font.cardTitle.copy(fontSize = 15.sp), + color = Amll.colors.ink, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + "共 $trackCount 首" + creator.takeIf { it.isNotBlank() }?.let { " · @$it" }.orEmpty(), + style = Font.micro, + color = Amll.colors.ink3, + ) + } + } + + Spacer(Modifier.height(12.dp)) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + PrimaryButton("播放全部", onClick = onPlayAll, modifier = Modifier.weight(1f)) + GhostButton("下载全部", onClick = onDownloadAll, modifier = Modifier.weight(1f)) + } + } +} + +@Composable +private fun DownloadProgressCard(state: DownloadStore.State) { + val finished = state.done + state.failed + val total = state.total.coerceAtLeast(1) + AmllCard(modifier = Modifier.padding(bottom = 8.dp), padding = 14.dp) { + if (state.running) { + if (state.currentName.isNotBlank()) { + Text( + state.currentName, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(4.dp)) + } + Text( + "$finished/${state.total}" + + if (state.failed > 0) " · 失败 ${state.failed}" else "", + style = Font.micro, + color = Amll.colors.ink3, + ) + Spacer(Modifier.height(8.dp)) + LinearProgressIndicator( + progress = { finished.toFloat() / total }, + modifier = Modifier.fillMaxWidth().height(6.dp), + color = Amll.colors.primary, + trackColor = Amll.colors.surface2, + ) + } + state.message?.let { + if (state.running) Spacer(Modifier.height(8.dp)) + Text(it, style = Font.micro, color = Amll.colors.ink2) + } + } +} + +@Composable +private fun NcmTrackRow( + track: NcmTrackDto, + current: Boolean, + onClick: () -> Unit, + onDownload: () -> Unit, +) { + val subtitle = track.displayArtist + + track.al?.name?.takeIf { it.isNotBlank() }?.let { " · $it" }.orEmpty() + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Radius.field)) + .background(if (current) Amll.colors.primarySoft else Color.Transparent) + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoverImage(track.displayCover.ifBlank { null }, track.name, 40.dp, radius = Radius.field) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + track.name.ifBlank { "未知歌曲" }, + style = Font.bodyMedium, + color = if (current) Amll.colors.primary else Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + subtitle, + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (track.dt > 0) { + Text(formatDuration(track.dt), style = Font.micro, color = Amll.colors.ink3) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + PlayCircleButton(size = 26.dp) + IconButton(onClick = onDownload, modifier = Modifier.size(28.dp)) { + Icon( + Icons.Rounded.Download, + "下载", + tint = Amll.colors.ink3, + modifier = Modifier.size(18.dp), + ) + } + } + } +} + +/** 毫秒 → m:ss */ +private fun formatDuration(ms: Long): String { + val duration = Duration.ofMillis(ms.coerceAtLeast(0)) + return String.format(Locale.US, "%d:%02d", duration.toMinutes(), duration.seconds % 60) +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseViewModel.kt new file mode 100644 index 0000000..12b04e1 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/NcmParseViewModel.kt @@ -0,0 +1,365 @@ +package dev.amll.hub.android.ui.screen + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.amll.hub.android.data.remote.NcmMusicDto +import dev.amll.hub.android.data.remote.NcmPlaylistDto +import dev.amll.hub.android.data.remote.NcmSongDto +import dev.amll.hub.android.data.remote.NcmTrackDto +import dev.amll.hub.android.data.repo.ContentRepository +import dev.amll.hub.android.data.repo.LyricRepository +import dev.amll.hub.android.data.repo.LyricSource +import dev.amll.hub.android.data.repo.PlaybackRepository +import dev.amll.hub.android.download.DownloadRequest +import dev.amll.hub.android.download.DownloadService +import dev.amll.hub.android.download.DownloadStore +import dev.amll.hub.android.playback.NcmQuality +import dev.amll.hub.android.playback.PlayerController +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * 网易云解析页的状态:三种入口共用一份,切换模式不清空彼此的结果, + * 来回切不用重新请求。 + */ +@HiltViewModel +class NcmParseViewModel @Inject constructor( + private val repo: ContentRepository, + private val playbackRepo: PlaybackRepository, + private val lyricRepo: LyricRepository, + private val player: PlayerController, + private val downloadStore: DownloadStore, + @ApplicationContext private val context: Context, +) : ViewModel() { + + /** 解析成功的单曲:音频信息 + 歌词预览原文 + 实际拿到的音质 */ + data class Parsed( + val songId: String, + val music: NcmMusicDto, + val source: LyricSource, + val actual: NcmQuality?, + ) + + data class UiState( + /** 0 关键词搜索 / 1 单曲 ID / 2 歌单 ID */ + val mode: Int = 0, + val query: String = "", + val searching: Boolean = false, + val results: List = emptyList(), + val songInput: String = "", + val parsing: Boolean = false, + val parsed: Parsed? = null, + val lyricLoading: Boolean = false, + val playlistInput: String = "", + val playlistLoading: Boolean = false, + val playlist: NcmPlaylistDto? = null, + val error: String? = null, + val quality: NcmQuality = NcmQuality.EXHIGH, + ) + + private val _state = MutableStateFlow(UiState()) + val state: StateFlow = _state.asStateFlow() + + /** 下载进度由前台服务写入,全部页面入口共用这份状态 */ + val downloadState: StateFlow = downloadStore.state + + /** 页面音质只影响后续解析 / 播放 / 下载,不重载正在播放的曲目 */ + fun setQuality(quality: NcmQuality) = _state.update { it.copy(quality = quality) } + + fun setMode(mode: Int) = _state.update { it.copy(mode = mode, error = null) } + + fun setQuery(value: String) = _state.update { it.copy(query = value, error = null) } + + fun setSongInput(value: String) = _state.update { it.copy(songInput = value, error = null) } + + fun setPlaylistInput(value: String) = + _state.update { it.copy(playlistInput = value, error = null) } + + fun dismissError() = _state.update { it.copy(error = null) } + + fun notifyError(message: String) = _state.update { it.copy(error = message) } + + /** 关键词搜索:相当于网页端「搜索」输入框 */ + fun search() { + val keyword = _state.value.query.trim() + if (keyword.isBlank()) return + _state.update { it.copy(searching = true, error = null) } + viewModelScope.launch { + repo.ncmSearch(keyword, limit = 30) + .onSuccess { songs -> + _state.update { + it.copy( + searching = false, + results = songs, + error = if (songs.isEmpty()) "没搜到结果,换个关键词试试" else null, + ) + } + } + .onFailure { e -> + // 把后端/网络的真实原因显示出来,不然「用不了」查不到原因 + _state.update { it.copy(searching = false, error = e.message ?: "搜索失败") } + } + } + } + + /** + * 单曲解析:输入可以是纯 ID、带 id 参数的链接,或者整段分享文案。 + * 音源走 PlaybackRepository(含 standard 兜底),歌词原文交给歌词页解析。 + */ + fun parseSong() { + val raw = _state.value.songInput.trim() + if (raw.isBlank()) return + val id = extractSongId(raw) + val quality = _state.value.quality + _state.update { + it.copy(parsing = true, parsed = null, lyricLoading = false, error = null) + } + viewModelScope.launch { + val resolved = playbackRepo.resolve(id, quality.key) + val music = resolved.music + if (music == null) { + _state.update { + it.copy( + parsing = false, + lyricLoading = false, + error = resolved.error ?: "解析失败", + ) + } + return@launch + } + + // 歌词拿不到不影响播放与下载,兜底成空歌词;先把结果卡亮出来, + // 歌词区域在加载期间显示骨架 + _state.update { + it.copy( + parsing = false, + parsed = Parsed(id, music, LyricSource(), resolved.actual), + lyricLoading = true, + ) + } + + val source = try { + lyricRepo.load(id, null, music.lyric, music.tlyric) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + LyricSource() + } + + _state.update { + it.copy( + parsing = false, + lyricLoading = false, + parsed = Parsed(id, music, source, resolved.actual), + error = if (music.displayUrl.isBlank()) { + "解析到了曲目,但没拿到播放地址(可能无版权)" + } else { + null + }, + ) + } + } + } + + /** 直接播放已解析的单曲,停留解析页;全局音质同步为页面选择 */ + fun playParsed() { + val parsed = _state.value.parsed ?: return + val quality = _state.value.quality + val music = parsed.music + player.setQuality(quality) + player.playResolved( + PlayerController.Track( + songId = parsed.songId, + title = music.displayName, + artist = music.displayArtist, + coverUrl = music.displayCover.ifBlank { null }, + streamUrl = music.displayUrl.takeIf { it.isNotBlank() }, + albumName = music.albumName, + lyricLrc = if (parsed.source.format == "lrc") parsed.source.text else "", + lyricTranslation = parsed.source.translation, + quality = quality, + actualQuality = parsed.actual, + ), + ) + } + + /** 搜索结果就地播放,不改变当前队列 */ + fun playSearch(song: NcmSongDto) { + val id = song.displayId + if (id.isBlank()) { + notifyError("没有拿到这首歌的 ID") + return + } + player.playSong(id, song.toQueueItem(), _state.value.quality) + } + + /** 播放全部:把歌单曲目换成一个新队列 */ + fun playPlaylistAll() { + val items = _state.value.playlist?.tracks.orEmpty().mapNotNull { it.toQueueItemOrNull() } + if (items.isEmpty()) { + notifyError("歌单里没有可播放的曲目") + return + } + player.playQueue(items, 0, _state.value.quality) + } + + /** 从歌单某一行开始播放:索引要映射到跳过空 ID 之后的队列下标 */ + fun playPlaylistTrack(index: Int) { + val tracks = _state.value.playlist?.tracks.orEmpty() + if (index !in tracks.indices) return + + val items = ArrayList(tracks.size) + var target = -1 + tracks.forEachIndexed { original, track -> + val item = track.toQueueItemOrNull() ?: return@forEachIndexed + if (original == index) target = items.size + items += item + } + if (target < 0) { + notifyError("这首歌没有可播放的 ID") + return + } + player.playQueue(items, target, _state.value.quality) + } + + fun parsePlaylist() { + val raw = _state.value.playlistInput.trim() + if (raw.isBlank()) return + _state.update { it.copy(playlistLoading = true, playlist = null, error = null) } + viewModelScope.launch { + repo.ncmPlaylist(raw) + .onSuccess { detail -> + _state.update { + it.copy( + playlistLoading = false, + playlist = detail, + error = null, + ) + } + } + .onFailure { e -> + _state.update { it.copy(playlistLoading = false, error = e.message ?: "歌单解析失败") } + } + } + } + + /* ===== 下载 ===== */ + + fun downloadParsed() { + val parsed = _state.value.parsed + if (parsed == null || parsed.songId.isBlank()) { + notifyError("先解析一首歌再下载") + return + } + val music = parsed.music + val request = DownloadRequest( + songId = parsed.songId, + name = music.displayName, + artists = music.displayArtist, + coverUrl = music.displayCover.ifBlank { null }, + albumName = music.albumName, + level = _state.value.quality.key, + ) + DownloadService.start(context, listOf(request), zip = false) + } + + fun downloadSearch(song: NcmSongDto) { + val id = song.displayId + if (id.isBlank()) { + notifyError("没有拿到这首歌的 ID") + return + } + val request = DownloadRequest( + songId = id, + name = song.displayName, + artists = song.displayArtist, + coverUrl = song.displayCover.ifBlank { null }, + albumName = song.displayAlbum, + level = _state.value.quality.key, + ) + DownloadService.start(context, listOf(request), zip = false) + } + + /** 歌单里单曲下载:按页面当前音质请求 */ + fun downloadPlaylistTrack(index: Int) { + val track = _state.value.playlist?.tracks?.getOrNull(index) + val request = track?.toDownloadRequest(_state.value.quality.key) + if (request == null) { + notifyError("这首歌没有可下载的 ID") + return + } + DownloadService.start(context, listOf(request), zip = false) + } + + fun downloadPlaylist() { + val detail = _state.value.playlist + val level = _state.value.quality.key + val requests = detail?.tracks.orEmpty().mapNotNull { it.toDownloadRequest(level) } + if (requests.isEmpty()) { + notifyError("歌单里没有可下载的曲目") + return + } + DownloadService.start( + context, + requests, + zip = true, + zipName = detail?.name.orEmpty().ifBlank { "歌单" }, + ) + } +} + +private fun NcmSongDto.toQueueItem(): PlayerController.QueueItem = + PlayerController.QueueItem( + songId = displayId, + name = displayName, + artists = displayArtist, + coverUrl = displayCover.ifBlank { null }, + albumName = displayAlbum, + ) + +/** 没有 ID 的曲目不能播放 / 下载,统一返回 null 由调用方跳过 */ +private fun NcmTrackDto.toQueueItemOrNull(): PlayerController.QueueItem? { + val id = displayId + if (id.isBlank()) return null + return PlayerController.QueueItem( + songId = id, + name = name.ifBlank { "未知歌曲" }, + artists = displayArtist, + coverUrl = displayCover.ifBlank { null }, + albumName = al?.name.orEmpty(), + ) +} + +private fun NcmTrackDto.toDownloadRequest(level: String): DownloadRequest? { + val id = displayId + if (id.isBlank()) return null + return DownloadRequest( + songId = id, + name = name.ifBlank { "未知歌曲" }, + artists = displayArtist, + coverUrl = displayCover.ifBlank { null }, + albumName = al?.name.orEmpty(), + level = level, + ) +} + +/** + * 从链接或分享文案里抠出纯数字 ID。 + * 纯数字直接返回;否则取 `id=xxx`,再退化为最长的一串数字。 + */ +internal fun extractSongId(raw: String): String { + val trimmed = raw.trim() + if (trimmed.all { it.isDigit() } && trimmed.isNotEmpty()) return trimmed + val idParam = Regex("[?&]id=(\\d+)").find(trimmed) + if (idParam != null) return idParam.groupValues[1] + val longest = Regex("\\d{5,}").findAll(trimmed).maxByOrNull { it.value.length } + return longest?.value ?: trimmed +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/PlaybackViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/PlaybackViewModel.kt new file mode 100644 index 0000000..bb5a88d --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/PlaybackViewModel.kt @@ -0,0 +1,53 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.playback.NcmQuality +import dev.amll.hub.android.playback.PlayerController +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +/** 播放态与播放控制:迷你播放器 / 歌词页共用 */ +@HiltViewModel +class PlaybackViewModel @Inject constructor( + private val player: PlayerController, +) : ViewModel() { + + val playerState: StateFlow = player.state + + fun togglePlay() { + val state = player.state.value + // 有曲目但播放器里没有可播内容(上次解析失败 / 无音源)时, + // 点播放就重新走一遍解析,而不是毫无反应 + if (!state.playing && player.player.mediaItemCount == 0 && state.track != null) { + player.replayCurrent() + return + } + player.toggle() + } + + fun seekTo(positionMs: Long) = player.seekTo(positionMs) + + fun next() = player.next() + + fun prev() = player.prev() + + fun toggleShuffle() = player.toggleShuffle() + + fun cycleRepeat() = player.cycleRepeatMode() + + fun playAt(index: Int) = player.playAt(index) + + fun removeAt(index: Int) = player.removeFromQueue(index) + + fun clearQueue() = player.clearQueue() + + fun playNcmSong(songId: String, meta: PlayerController.QueueItem) = + player.playSong(songId, meta) + + fun playQueue(items: List) = player.playQueue(items) + + fun setQuality(quality: NcmQuality) = player.setQuality(quality) + + fun replayCurrent() = player.replayCurrent() +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileEditScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileEditScreen.kt new file mode 100644 index 0000000..c7a554f --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileEditScreen.kt @@ -0,0 +1,85 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.AmllTextField +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.FieldRow +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font + +/* ============================ 12 资料编辑 ============================ */ + +@Composable +fun ProfileEditScreen(vm: ProfileViewModel, nav: AppNav) { + LaunchedEffect(Unit) { vm.loadProfile() } + val profile by vm.profile.collectAsStateWithLifecycle() + var name by remember(profile) { mutableStateOf(profile?.displayName.orEmpty()) } + var saving by remember { mutableStateOf(false) } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + AmllTopBar("编辑资料", onBack = nav.back) + Avatar(profile?.avatar, 82.dp) + Text("更换头像", style = Font.caption.copy(fontWeight = FontWeight.Medium), color = Amll.colors.primary) + AmllCard(padding = 16.dp) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + AmllTextField( + value = name, + onValueChange = { name = it }, + placeholder = "输入昵称", + label = "昵称", + imeAction = ImeAction.Done, + ) + FieldRow("用户名", profile?.name.orEmpty().ifBlank { "—" }) + } + } + Text( + "用户名注册后不可修改;昵称会显示在评论与投稿记录里。", + style = Font.micro, + color = Amll.colors.ink3, + modifier = Modifier.fillMaxWidth(), + ) + PrimaryButton( + text = if (saving) "保存中…" else "保存修改", + enabled = name.isNotBlank() && !saving, + onClick = { + saving = true + vm.saveDisplayName(name) { ok -> + saving = false + if (ok) nav.back() + } + }, + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileScreen.kt new file mode 100644 index 0000000..7ef8d88 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileScreen.kt @@ -0,0 +1,237 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.QrCodeScanner +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.UserActivityDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.TabScaffold +import dev.amll.hub.android.ui.bottomBarPadding +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.NavRow +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import java.time.LocalDate + +/* ============================ 11 我的 ============================ */ + +@Composable +fun ProfileScreen(vm: ProfileViewModel, nav: AppNav) { + LaunchedEffect(Unit) { + vm.loadProfile() + vm.loadActivity() + } + val profile by vm.profile.collectAsStateWithLifecycle() + val activity by vm.activity.collectAsStateWithLifecycle() + val activityLoading by vm.activityLoading.collectAsStateWithLifecycle() + val displayName = profile?.displayName?.takeIf { it.isNotBlank() } + ?: profile?.name?.takeIf { it.isNotBlank() } + ?: "未登录" + val role = when { + profile?.isAdmin == true -> "管理员" + profile?.isReviewer == true -> "审核员" + else -> "普通用户" + } + + TabScaffold { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding( + start = 20.dp, + end = 20.dp, + top = 14.dp, + bottom = bottomBarPadding(16.dp), + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp)) { + Avatar(profile?.avatar, 62.dp) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) { + Text(displayName, style = Font.pageTitle.copy(fontSize = 20.sp), color = Amll.colors.ink) + Text( + "@" + profile?.name.orEmpty().ifBlank { "amll" } + " · " + role, + style = Font.caption, + color = Amll.colors.ink3, + ) + } + Icon( + Icons.Rounded.QrCodeScanner, + "扫一扫", + tint = Amll.colors.ink2, + modifier = Modifier.size(20.dp).clickable(onClick = nav.openScan), + ) + Spacer(Modifier.width(10.dp)) + Box( + Modifier + .clip(RoundedCornerShape(14.dp)) + .background(Amll.colors.surface) + .border(1.dp, Amll.colors.border, RoundedCornerShape(14.dp)) + .clickable(onClick = nav.openProfileEdit) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) { + Text("编辑资料", style = Font.micro.copy(fontWeight = FontWeight.Medium), color = Amll.colors.ink2) + } + } + + AmllCard(padding = 16.dp) { + Text("账号概览", style = Font.cardTitle, color = Amll.colors.ink) + Spacer(Modifier.height(6.dp)) + listOf( + "用户名" to profile?.name.orEmpty().ifBlank { "—" }, + "昵称" to displayName, + "邮箱" to profile?.email.orEmpty().ifBlank { "未绑定" }, + "手机号" to profile?.phone.orEmpty().ifBlank { "未绑定" }, + "身份" to role, + ).forEach { (label, value) -> + Row( + Modifier.fillMaxWidth().height(42.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(label, style = Font.bodySmall, color = Amll.colors.ink3) + Text( + value, + style = Font.bodyMedium, + color = if (label == "身份") Amll.colors.warning else Amll.colors.ink, + ) + } + } + } + + AmllCard(padding = 16.dp) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("活动记录", style = Font.cardTitle, color = Amll.colors.ink, modifier = Modifier.weight(1f)) + Text( + activity?.year?.let { "$it 年" } ?: "", + style = Font.micro, + color = Amll.colors.ink3, + ) + } + Spacer(Modifier.height(12.dp)) + ActivityHeatmap(activity) + Spacer(Modifier.height(10.dp)) + Text( + activitySummary(activity, activityLoading), + style = Font.micro, + color = Amll.colors.ink3, + ) + } + + AmllCard(padding = 16.dp) { + NavRow("我的信息", onClick = nav.openProfileEdit) + NavRow("账号安全", onClick = nav.openProfileEdit) + NavRow("审核中心", onClick = nav.openReview) + } + } + } +} + +/** 活动贡献墙:最近 20 周 × 7 天,颜色深浅对应当天次数 */ +@Composable +private fun ActivityHeatmap(activity: UserActivityDto?) { + val counts = remember(activity) { + activity?.days?.associate { it.date to it.count }.orEmpty() + } + val today = remember { LocalDate.now() } + val start = remember(today) { + today.with(java.time.DayOfWeek.MONDAY).minusWeeks(19) + } + + val ramp = heatRamp() + + BoxWithConstraints(Modifier.fillMaxWidth()) { + val gap = 3.dp + val cell = (maxWidth - gap * 19f) * (1f / 20f) + val cellPx = with(LocalDensity.current) { cell.toPx() } + val gapPx = with(LocalDensity.current) { gap.toPx() } + val radius = with(LocalDensity.current) { 3.dp.toPx() } + // 一次 Canvas 画完 20×7 格:140 个 Box 会产生同样多的布局与语义节点 + Canvas(Modifier.fillMaxWidth().height(cell * 7 + gap * 6)) { + for (col in 0 until 20) { + for (row in 0 until 7) { + val date = start.plusDays((col * 7 + row).toLong()) + if (date.isAfter(today)) continue + val count = counts[date.toString()] ?: 0 + drawRoundRect( + color = heatColor(count, ramp), + topLeft = Offset(col * (cellPx + gapPx), row * (cellPx + gapPx)), + size = Size(cellPx, cellPx), + cornerRadius = CornerRadius(radius), + ) + } + } + } + } +} + +@Composable +private fun heatRamp(): List = listOf( + Amll.colors.surface2, + Amll.colors.primary.copy(alpha = 0.28f), + Amll.colors.primary.copy(alpha = 0.5f), + Amll.colors.primary.copy(alpha = 0.75f), + Amll.colors.primary, +) + +/** 有序色阶:按当天次数取档,避免把颜色写死在 when 里 */ +private fun heatColor(count: Int, ramp: List): Color { + val index = when { + count <= 0 -> 0 + count == 1 -> 1 + count <= 3 -> 2 + count <= 6 -> 3 + else -> 4 + } + return ramp[index] +} + +private fun activitySummary(activity: UserActivityDto?, loading: Boolean): String { + if (activity == null) return if (loading) "正在加载活动记录…" else "暂无活动记录" + val byType = activity.byType + val parts = listOfNotNull( + byType["lyric"]?.takeIf { it > 0 }?.let { "投稿歌词 $it 次" }, + byType["daily"]?.takeIf { it > 0 }?.let { "每日推荐 $it 次" }, + byType["review"]?.takeIf { it > 0 }?.let { "审核 $it 次" }, + byType["search_ip"]?.takeIf { it > 0 }?.let { "歌词 IP $it 次" }, + ) + return if (parts.isEmpty()) "今年还没有留下记录" else "今年共 ${activity.total} 次 · " + parts.joinToString(" / ") +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileViewModel.kt new file mode 100644 index 0000000..b390eab --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ProfileViewModel.kt @@ -0,0 +1,51 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.AuthRepository +import dev.amll.hub.android.data.remote.UserActivityDto +import dev.amll.hub.android.data.remote.UserProfile +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** 资料 / 活动记录:首页头像、我的、资料编辑共用同一份缓存 */ +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val repo: ContentRepository, + private val authRepo: AuthRepository, +) : ViewModel() { + + val profile = MutableStateFlow(null) + + /** 活动记录(真实数据,来自 /users/me/activity) */ + val activity = MutableStateFlow(null) + val activityLoading = MutableStateFlow(false) + + fun loadProfile() { + if (profile.value != null) return + viewModelScope.launch { profile.value = runCatching { authRepo.profile() }.getOrNull() } + } + + fun loadActivity(year: Int? = null) { + if (activityLoading.value) return + // 已有当年数据就不再重复请求;显式传 year 时才刷新 + if (year == null && activity.value != null) return + viewModelScope.launch { + activityLoading.value = true + val data = repo.activity(year).getOrNull() + activity.value = data + activityLoading.value = false + } + } + + fun saveDisplayName(name: String, onDone: (Boolean) -> Unit = {}) { + viewModelScope.launch { + val updated = repo.updateDisplayName(name).getOrNull() + if (updated != null) profile.value = updated + onDone(updated != null) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingScreen.kt new file mode 100644 index 0000000..1dd40e2 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingScreen.kt @@ -0,0 +1,193 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.CoverImage +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.CardShape +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius + +/* ============================ 03 排行榜 ============================ */ + +@Composable +fun RankingScreen(vm: RankingViewModel, nav: AppNav) { + val days by vm.rankingDays.collectAsStateWithLifecycle() + val ui by vm.ranking.collectAsStateWithLifecycle() + LaunchedEffect(days) { vm.loadRanking(days) } + + Column(Modifier.fillMaxSize().background(Amll.colors.canvas)) { + Column( + Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 14.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AmllTopBar("无歌词排行榜", onBack = nav.back) { + Text("每周一清空", style = Font.caption, color = Amll.colors.ink3) + } + + Row( + Modifier + .fillMaxWidth() + .height(36.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Amll.colors.surface2) + .padding(3.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + listOf(1, 3, 7).forEach { d -> + val active = d == days + Box( + Modifier + .weight(1f) + .fillMaxSize() + .clip(RoundedCornerShape(9.dp)) + .background(if (active) Amll.colors.surface else Color.Transparent) + .clickable { vm.loadRanking(d) }, + contentAlignment = Alignment.Center, + ) { + Text( + "$d 天", + style = Font.caption.copy(fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium), + color = if (active) Amll.colors.ink else Amll.colors.ink3, + ) + } + } + } + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text("按请求次数", style = Font.micro.copy(fontWeight = FontWeight.Bold), color = Amll.colors.ink3) + Text("最近 $days 天", style = Font.micro, color = Amll.colors.ink3) + } + + when { + ui.loading -> SkeletonList(5) + ui.items.isEmpty() -> EmptyState( + icon = Icons.Outlined.Inbox, + title = "暂无榜单数据", + description = "换个时间范围试试", + actionText = "重新加载", + onAction = { vm.loadRanking(days) }, + ) + else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(2.dp)) { + item { ChampionCard(ui.items.first(), nav) } + itemsIndexed(ui.items.drop(1)) { index, item -> + RankingRow(rank = index + 2, item = item, nav = nav) + } + } + } + } + } +} + +@Composable +private fun ChampionCard(item: RankingItemDto, nav: AppNav) { + Row( + Modifier + .fillMaxWidth() + .height(92.dp) + .clip(CardShape) + .background(Brush.linearGradient(listOf(Color(0xFFE0303F), Color(0xFF59162E)))) + .clickable { nav.openLyric(item.platformId, item.songName, item.artists.joinToString("/"), item.cover) } + .padding(horizontal = 18.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(5.dp)) { + Text("请求最多", style = Font.micro, color = Color.White) + Text(item.songName, style = Font.navTitle.copy(fontSize = 19.sp), color = Color.White, maxLines = 1) + Text( + "${item.artists.joinToString(" / ")} · ${item.requestCount} 次请求", + style = Font.micro, + color = Color.White.copy(alpha = 0.8f), + ) + } + Box( + Modifier.size(40.dp).clip(CircleShape).background(Color.White), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Rounded.PlayArrow, "播放", tint = Amll.colors.primary, modifier = Modifier.size(22.dp)) + } + } +} + +@Composable +private fun RankingRow(rank: Int, item: RankingItemDto, nav: AppNav) { + Row( + Modifier + .fillMaxWidth() + .height(58.dp) + .clickable { nav.openLyric(item.platformId, item.songName, item.artists.joinToString("/"), item.cover) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + rank.toString().padStart(2, '0'), + style = Font.caption.copy(fontWeight = FontWeight.Bold), + color = Amll.colors.ink3, + modifier = Modifier.width(20.dp), + ) + CoverImage(item.cover.ifBlank { null }, item.songName, 40.dp, radius = Radius.field) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text(item.songName, style = Font.bodyMedium, color = Amll.colors.ink, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + "${item.artists.joinToString(" / ")} · ${item.platform} ${item.platformId}", + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text("${item.requestCount} 次", style = Font.caption.copy(fontWeight = FontWeight.Medium), color = Amll.colors.ink2) + Box( + Modifier.size(20.dp).clip(CircleShape).border(1.dp, Amll.colors.border, CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Rounded.PlayArrow, "播放", tint = Amll.colors.ink3, modifier = Modifier.size(12.dp)) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingViewModel.kt new file mode 100644 index 0000000..117ad53 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/RankingViewModel.kt @@ -0,0 +1,37 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.RankingItemDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RankingViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val ranking = MutableStateFlow(ListUi()) + val rankingDays = MutableStateFlow(7) + + fun loadRanking(days: Int = rankingDays.value) { + rankingDays.value = days + viewModelScope.launch { + ranking.update { it.copy(loading = true, error = null) } + val result = repo.ranking(days) + val items = result.getOrDefault(emptyList()) + ranking.update { + it.copy( + loading = false, + items = items, + error = result.exceptionOrNull()?.message + ?: if (items.isEmpty()) "暂无榜单数据" else null, + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ReviewQueueScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ReviewQueueScreen.kt new file mode 100644 index 0000000..ac0243a --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ReviewQueueScreen.kt @@ -0,0 +1,139 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.ChipTone +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.component.StatusChip +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.ChipShape +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius + +/* ============================ 20 审核队列 ============================ */ + +@Composable +fun ReviewQueueScreen(vm: CreatorViewModel, nav: AppNav) { + LaunchedEffect(Unit) { if (vm.submissions.value.items.isEmpty()) vm.loadSubmissions() } + val ui by vm.submissions.collectAsStateWithLifecycle() + var typeIndex by remember { mutableIntStateOf(0) } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + AmllTopBar("审核中心", onBack = nav.back) { + Text("筛选", style = Font.caption, color = Amll.colors.ink3) + } + + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + listOf("歌词投稿", "每日推荐", "搜索 IP").forEachIndexed { index, label -> + val active = index == typeIndex + Box( + Modifier + .clip(ChipShape) + .background(if (active) Amll.colors.primarySoft else Amll.colors.surface2) + .clickable { typeIndex = index } + .padding(horizontal = 12.dp, vertical = 7.dp), + ) { + Text( + label, + style = Font.micro.copy(fontWeight = FontWeight.Medium), + color = if (active) Amll.colors.primary else Amll.colors.ink3, + ) + } + } + } + + val pendingCount = ui.items.count { it.status == "pending" } + val reviewingCount = ui.items.count { it.status == "reviewing" } + val revisedCount = ui.items.count { it.status == "revised" } + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + StatusChip("待审核 $pendingCount", ChipTone.PRIMARY) + StatusChip("审核中 $reviewingCount", ChipTone.NEUTRAL) + StatusChip("待确认 $revisedCount", ChipTone.NEUTRAL) + } + + when { + ui.loading -> SkeletonList(4) + ui.items.isEmpty() -> EmptyState(Icons.Outlined.Inbox, "暂无稿件", "换个筛选条件试试") + else -> Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + ui.items.forEach { item -> + Row( + Modifier.fillMaxWidth().height(74.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + item.displayName.ifBlank { "未命名稿件" }, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + ) + Text( + "${item.artist.ifBlank { "未知歌手" }} · 投稿人 ${item.submitter.ifBlank { "匿名" }}", + style = Font.micro, + color = Amll.colors.ink3, + ) + Text( + "已等待 ${item.createdAt.take(10)}", + style = Font.micro, + color = Amll.colors.ink3.copy(alpha = 0.7f), + ) + } + val (label, tone) = statusOf(item.status) + StatusChip(label, tone) + } + } + } + } + + Box( + Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(Radius.field)) + .background(Amll.colors.surface2) + .padding(horizontal = 12.dp), + contentAlignment = Alignment.CenterStart, + ) { + Text("进入详情即占用该稿件,离开或超时自动释放", style = Font.micro, color = Amll.colors.ink3) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanScreen.kt index 66ebcfd..a361b38 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanScreen.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanScreen.kt @@ -1,46 +1,77 @@ package dev.amll.hub.android.ui.screen +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button +import androidx.compose.material.icons.rounded.Badge +import androidx.compose.material.icons.rounded.ChevronLeft +import androidx.compose.material.icons.rounded.PhotoLibrary import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.amll.hub.android.ui.component.ErrorBanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage import dev.amll.hub.android.ui.component.QrScannerView +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius /** - * 扫码登录页:内嵌 CameraX + ML Kit 扫描器。 + * 扫一扫:整屏取景 + 中间扫描窗 + 底部两个圆形入口。 */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ScanScreen( onBack: () -> Unit, @@ -50,156 +81,337 @@ fun ScanScreen( viewModel: ScanViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() + val context = LocalContext.current + var galleryError by remember { mutableStateOf(null) } + var showCard by remember { mutableStateOf(false) } + val profile by viewModel.profile.collectAsStateWithLifecycle() - // deep link 场景:已经拿到 ticket,直接进入确认态 LaunchedEffect(initialTicket) { initialTicket?.takeIf { it.isNotBlank() }?.let(viewModel::onScanned) } + LaunchedEffect(Unit) { viewModel.loadProfile() } - Scaffold( - topBar = { - TopAppBar( - title = { Text("扫码登录") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - }, + // 相册识别:选一张图丢给 ML Kit,识别到二维码就当成扫到处理 + val galleryPicker = rememberLauncherForActivityResult( + ActivityResultContracts.PickVisualMedia(), + ) { uri: Uri? -> + if (uri == null) return@rememberLauncherForActivityResult + val image = runCatching { InputImage.fromFilePath(context, uri) }.getOrNull() + if (image == null) { + galleryError = "这张图片读不出来" + return@rememberLauncherForActivityResult + } + val scanner = BarcodeScanning.getClient( + BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build(), + ) + scanner.process(image) + .addOnSuccessListener { codes -> + val value = codes.firstOrNull()?.rawValue + if (value.isNullOrBlank()) galleryError = "图里没有识别到二维码" else viewModel.onScanned(value) + } + .addOnFailureListener { galleryError = "识别失败,换张图试试" } + .addOnCompleteListener { scanner.close() } + } + + // 授权流程中关掉取景框,避免相机常驻 + val showCamera = state !is ScanState.AwaitingConfirm && + state !is ScanState.Submitting && + state !is ScanState.Success + + Box(Modifier.fillMaxSize().background(Color.Black)) { + if (showCamera) { + QrScannerView( + onQrScanned = viewModel::onScanned, + modifier = Modifier.fillMaxSize(), ) - }, - ) { padding -> - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .fillMaxSize() - .padding(padding), + } + + // 顶部:返回 + 标题 + Row( + Modifier + .fillMaxWidth() + .statusBarsPadding() + .height(56.dp) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - // state是by 委托属性,when 分支内无法 smart cast,先落到局部变量 - when (val current = state) { - // 扫描器只在「等待扫码 / 出错重扫」时显示 - is ScanState.Idle -> ScannerPane( - errorMessage = null, - onQrScanned = viewModel::onScanned, - onDismissError = null, - onBack = onBack, - ) + Icon( + Icons.Rounded.ChevronLeft, + "返回", + tint = Color.White, + modifier = Modifier.size(28.dp).clickable(onClick = onBack), + ) + Text( + "扫描二维码", + style = Font.navTitle.copy(fontSize = 19.sp, fontWeight = FontWeight.SemiBold), + color = Color.White, + modifier = Modifier.weight(1f), + ) + // 与返回键等宽,标题才真正居中 + Spacer(Modifier.size(28.dp)) + } - is ScanState.Error -> ScannerPane( - errorMessage = current.message, - onQrScanned = viewModel::onScanned, - onDismissError = viewModel::reset, - onBack = onBack, - ) + // 中间:上下扫动的高亮线,颜色跟随站点主题色 + if (showCamera) { + val transition = rememberInfiniteTransition(label = "scan") + val progress by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(2400, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = "scanProgress", + ) + // 颜色取主题色再往白里调,避免饱和度过高 + val base = Amll.colors.primary + val lineColor = lerp(base, Color.White, 0.55f) + val glowCore = lerp(base, Color.White, 0.42f) + val glowMid = lerp(base, Color.White, 0.15f) - // 确认 / 提交 / 成功都由弹窗接管,背景留空 - else -> Unit - } + Box( + Modifier + .fillMaxSize() + // 扫描线跑在「顶栏下沿 → 底部按钮上沿」之间 + .windowInsetsPadding(WindowInsets.statusBars) + .navigationBarsPadding() + .padding(top = 56.dp, bottom = 140.dp) + // 光带会溢出节点边界,裁掉才不会糊到标题和按钮上 + .clipToBounds() + .drawBehind { + val y = size.height * progress + val radius = size.width * 0.55f + + // 还原设计稿:一条横贯屏幕的椭圆光晕, + // 做法是把圆形径向渐变纵向压扁(1 : 0.16) + scale(scaleX = 1f, scaleY = 0.16f, pivot = Offset(size.width / 2f, y)) { + drawRect( + brush = Brush.radialGradient( + colors = listOf( + glowCore.copy(alpha = 0.75f), + glowMid.copy(alpha = 0.30f), + Color.Transparent, + ), + center = Offset(size.width / 2f, y), + radius = radius, + ), + topLeft = Offset(0f, y - radius), + size = Size(size.width, radius * 2f), + ) + } + + // 中心的细亮线 + drawRect( + color = lineColor.copy(alpha = 0.9f), + topLeft = Offset(0f, y - 0.6.dp.toPx()), + size = Size(size.width, 1.2.dp.toPx()), + ) + }, + ) } - } - // 扫码成功 → 确认授权 - val awaiting = state as? ScanState.AwaitingConfirm - if (awaiting != null) { - AlertDialog( - onDismissRequest = viewModel::cancel, - title = { Text("确认授权") }, - text = { Text("是否允许当前账号登录 AMLL Hub 网页端?确认后网页会自动完成登录。") }, - confirmButton = { - TextButton(onClick = viewModel::confirm) { Text("确认登录") } - }, - dismissButton = { - TextButton(onClick = viewModel::cancel) { Text("取消") } - }, - ) - } + // 状态提示(授权 / 错误) + val hint = when (val s = state) { + is ScanState.AwaitingConfirm -> "已扫码,等待确认" + is ScanState.Submitting -> "正在确认…" + is ScanState.Success -> "已授权" + is ScanState.Error -> s.message + else -> galleryError + } + if (hint != null) { + Text( + hint, + style = Font.caption, + color = if (state is ScanState.Error || galleryError != null) Amll.colors.warning else Color.White.copy(alpha = 0.8f), + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 40.dp), + ) + } + + // 底部两个圆形入口 + Row( + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .navigationBarsPadding() + .padding(start = 56.dp, end = 56.dp, bottom = 56.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ScanAction( + label = "我的名片", + icon = { tint -> + Icon(Icons.Rounded.Badge, null, tint = tint, modifier = Modifier.size(24.dp)) + }, + onClick = { showCard = true }, + ) + ScanAction( + label = "相册", + icon = { tint -> + Icon(Icons.Rounded.PhotoLibrary, null, tint = tint, modifier = Modifier.size(24.dp)) + }, + onClick = { + galleryError = null + galleryPicker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + }, + ) + } - // 提交中 - if (state is ScanState.Submitting) { - AlertDialog( - onDismissRequest = {}, - confirmButton = {}, - text = { + if (state is ScanState.Submitting) { + Dialog(onDismissRequest = {}) { Column( + Modifier + .clip(RoundedCornerShape(Radius.sheet)) + .background(Amll.colors.surface) + .padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(14.dp), ) { - CircularProgressIndicator() - Spacer(Modifier.height(16.dp)) - Text("正在确认…") + CircularProgressIndicator( + modifier = Modifier.size(22.dp), + strokeWidth = 2.dp, + color = Amll.colors.primary, + ) + Text("正在确认…", style = Font.caption, color = Amll.colors.ink3) } - }, - ) + } + } } - // 成功 - val success = state as? ScanState.Success - if (success != null) { - AlertDialog( - onDismissRequest = {}, - confirmButton = { TextButton(onClick = onDone) { Text("完成") } }, - icon = { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, + // 扫码成功 → 确认授权 + val awaiting = state as? ScanState.AwaitingConfirm + if (awaiting != null) { + ScanDialog( + title = "确认授权", + description = "是否允许当前账号登录 AMLL Hub 网页端?确认后网页会自动完成登录。", + ) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + ScanDialogButton( + text = "取消", + primary = false, + onClick = viewModel::cancel, + modifier = Modifier.weight(1f), ) - }, - title = { Text("已授权") }, - text = { - Text( - success.profile - ?.let { "已允许 ${it.displayName.ifBlank { it.name }} 登录网页端" } - ?: "网页端登录成功" + ScanDialogButton( + text = "确认登录", + primary = true, + onClick = viewModel::confirm, + modifier = Modifier.weight(1f), ) - }, - ) + } + } } -} -@Composable -private fun ScannerPane( - errorMessage: String?, - onQrScanned: (String) -> Unit, - onDismissError: (() -> Unit)?, - onBack: () -> Unit, -) { - Column(modifier = Modifier.fillMaxSize()) { - if (errorMessage != null) { - ErrorBanner( - message = errorMessage, - modifier = Modifier.padding(16.dp), + val success = state as? ScanState.Success + if (success != null) { + ScanDialog( + title = "已授权", + description = success.profile?.let { + "已允许 ${it.displayName.ifBlank { it.name }} 登录网页端" + } ?: "网页端登录成功", + ) { + ScanDialogButton( + text = "完成", + primary = true, + onClick = onDone, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + // 我的名片:先展示当前账号信息,二维码功能还没接 + if (showCard) { + ScanDialog(title = "我的名片", description = "当前登录的 AMLL Hub 账号") { + Text( + profile?.displayName?.ifBlank { profile?.name.orEmpty() } ?: "未登录", + style = Font.sheetTitle, + color = Amll.colors.ink, + ) + Text( + profile?.name?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: "—", + style = Font.caption, + color = Amll.colors.ink3, + ) + ScanDialogButton( + text = "关闭", + primary = false, + onClick = { showCard = false }, + modifier = Modifier.fillMaxWidth(), ) } + } +} +/** 底部圆形入口:深灰圆 + 白色图标 + 小字 */ +@Composable +private fun ScanAction( + label: String, + icon: @Composable (Color) -> Unit, + onClick: () -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.clickable(onClick = onClick), + ) { Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f), + Modifier + .size(58.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, ) { - QrScannerView(onQrScanned = onQrScanned) + icon(Color.White) } + Text(label, style = Font.micro.copy(fontSize = 11.sp), color = Color.White) + } +} +@Composable +private fun ScanDialog( + title: String, + description: String, + content: @Composable () -> Unit, +) { + Dialog(onDismissRequest = {}) { Column( - horizontalAlignment = Alignment.CenterHorizontally, + Modifier + .clip(RoundedCornerShape(Radius.sheet)) + .background(Amll.colors.surface) + .padding(20.dp), verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), ) { - Text( - text = "将 AMLL Hub 网页端登录弹窗的二维码放入框内", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - if (onDismissError != null) { - Button(onClick = onDismissError, modifier = Modifier.fillMaxWidth()) { - Text("继续扫描") - } - } - OutlinedButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { - Text("返回") - } + Text(title, style = Font.sheetTitle, color = Amll.colors.ink) + Text(description, style = Font.caption, color = Amll.colors.ink3) + Spacer(Modifier.height(2.dp)) + content() } } } + +@Composable +private fun ScanDialogButton( + text: String, + primary: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier + .height(46.dp) + .clip(RoundedCornerShape(Radius.chip)) + .background(if (primary) Amll.colors.primary else Amll.colors.surface2) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Text( + text, + style = Font.bodyMedium, + color = if (primary) Amll.colors.onPrimary else Amll.colors.ink2, + ) + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt index e5da614..3bf893f 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/ScanViewModel.kt @@ -3,6 +3,7 @@ package dev.amll.hub.android.ui.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.QrTicketParser import dev.amll.hub.android.data.remote.AuthRepository import dev.amll.hub.android.data.remote.UserProfile import kotlinx.coroutines.flow.MutableStateFlow @@ -41,12 +42,20 @@ class ScanViewModel @Inject constructor( private val _state = MutableStateFlow(ScanState.Idle) val state: StateFlow = _state.asStateFlow() + /** 「我的名片」用当前账号信息 */ + private val _profile = MutableStateFlow(null) + val profile: StateFlow = _profile.asStateFlow() + + fun loadProfile() { + viewModelScope.launch { _profile.value = runCatching { repo.profile() }.getOrNull() } + } + /** * 解析扫码结果,取出 ticket。 * 兼容 amllhub://qrlogin?ticket=xxx 与直接传 ticket 字符串。 */ fun onScanned(raw: String) { - val ticket = extractTicket(raw) + val ticket = QrTicketParser.parse(raw) if (ticket.isBlank()) { _state.value = ScanState.Error("这不是 AMLL Hub 的登录二维码") return @@ -86,15 +95,4 @@ class ScanViewModel @Inject constructor( fun reset() { _state.value = ScanState.Idle } - - private fun extractTicket(raw: String): String { - val trimmed = raw.trim() - val marker = "ticket=" - val index = trimmed.indexOf(marker) - return if (index >= 0) { - trimmed.substring(index + marker.length).substringBefore('&').trim() - } else { - trimmed - } - } } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchScreen.kt new file mode 100644 index 0000000..72ae54b --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchScreen.kt @@ -0,0 +1,261 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material.icons.rounded.ChevronLeft +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.data.remote.SearchHitDto +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.EmptyState +import dev.amll.hub.android.ui.component.SearchPillField +import dev.amll.hub.android.ui.component.SkeletonList +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font +import dev.amll.hub.android.ui.theme.Radius +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged + +/** 搜索范围,取值与后端 /search?field= 一致 */ +private val SEARCH_FIELDS = listOf( + "all" to "全部", + "song" to "歌曲", + "artist" to "歌手", + "album" to "专辑", + "lyric" to "歌词", + "id" to "音乐 ID", +) + +/** + * 全屏搜索页:对齐网页端的「歌词库搜索」,命中后直接进歌词页。 + */ +@OptIn(FlowPreview::class) +@Composable +fun SearchScreen(vm: SearchViewModel, nav: AppNav) { + val state by vm.search.collectAsStateWithLifecycle() + var query by remember { mutableStateOf(state.query) } + var hint by remember { mutableStateOf(null) } + val focus = remember { FocusRequester() } + + LaunchedEffect(Unit) { focus.requestFocus() } + // 输入停顿 320ms 后再请求,避免每个字符都打一次接口 + LaunchedEffect(Unit) { + snapshotFlow { query } + .debounce(320) + .distinctUntilChanged() + .collectLatest { q -> if (q.isNotBlank()) vm.runSearch(q) } + } + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .imePadding(), + ) { + Row( + Modifier + .fillMaxWidth() + .height(58.dp) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Rounded.ChevronLeft, + "返回", + tint = Amll.colors.ink, + modifier = Modifier.size(22.dp).clickable(onClick = nav.back), + ) + SearchPillField( + value = query, + onValueChange = { + query = it + hint = null + }, + placeholder = "搜歌名、歌手、歌词片段", + modifier = Modifier.weight(1f), + focusRequester = focus, + onSearch = { vm.runSearch(query) }, + onClear = { + query = "" + hint = null + vm.runSearch("") + }, + ) + Text( + "取消", + style = Font.caption, + color = Amll.colors.ink2, + modifier = Modifier.clickable(onClick = nav.back), + ) + } + + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + SEARCH_FIELDS.forEach { (value, label) -> + val active = state.field == value + Box( + Modifier + .height(28.dp) + .clip(RoundedCornerShape(Radius.chip)) + .background(if (active) Amll.colors.primarySoft else Amll.colors.surface2) + .clickable(enabled = !active) { + vm.runSearch(query, value) + } + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center, + ) { + Text( + label, + style = Font.micro, + color = if (active) Amll.colors.primary else Amll.colors.ink3, + ) + } + } + } + + hint?.let { + Text( + it, + style = Font.micro, + color = Amll.colors.warning, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + ) + } + + val hits = state.hits + when { + state.loading -> Column( + Modifier.fillMaxSize().padding(horizontal = 20.dp, vertical = 8.dp), + ) { + SkeletonList(5) + } + + hits.isNotEmpty() -> LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + item { + Text( + "找到 ${hits.size} 条歌词", + style = Font.micro, + color = Amll.colors.ink3, + modifier = Modifier.padding(vertical = 8.dp), + ) + } + items(hits) { hit -> + SearchHitRow(hit) { + val ncmId = hit.platformIds["ncm"]?.firstOrNull() + if (ncmId.isNullOrBlank()) { + hint = "这条歌词没有关联网易云 ID,暂时无法播放" + } else { + nav.openLyricFile( + ncmId, + hit.musicNames.firstOrNull().orEmpty(), + hit.artists.joinToString("/"), + null, + hit.rawLyricFile.ifBlank { null }, + ) + } + } + } + } + + state.error != null -> EmptyState( + icon = Icons.Outlined.Inbox, + title = state.error.orEmpty(), + description = "换个关键词试试,比如歌名或歌手", + ) + + else -> EmptyState( + icon = Icons.Rounded.Search, + title = "搜索站内歌词库", + description = "支持歌名、歌手与歌词片段", + ) + } + } +} + +@Composable +private fun SearchHitRow(hit: SearchHitDto, onClick: () -> Unit) { + Row( + Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + // 词库结果没有封面,就不放占位块了,直接把文字铺开 + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + hit.musicNames.firstOrNull() ?: hit.id, + style = Font.bodyMedium, + color = Amll.colors.ink, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + hit.artists.joinToString(" / ").ifBlank { "未知歌手" } + + " · ${hit.lineCount} 行 · ${hit.wordCount} 字", + style = Font.micro, + color = Amll.colors.ink3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (hit.lyricSnippet.isNotBlank()) { + Text( + hit.lyricSnippet, + style = Font.caption, + color = Amll.colors.ink3.copy(alpha = 0.8f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchViewModel.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchViewModel.kt new file mode 100644 index 0000000..5d10fad --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SearchViewModel.kt @@ -0,0 +1,56 @@ +package dev.amll.hub.android.ui.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.amll.hub.android.data.remote.SearchHitDto +import dev.amll.hub.android.data.repo.ContentRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class SearchUi( + val query: String = "", + val loading: Boolean = false, + /** all / song / artist / album / lyric / id,与网页端一致 */ + val field: String = "all", + val hits: List = emptyList(), + val error: String? = null, +) + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val repo: ContentRepository, +) : ViewModel() { + + val search = MutableStateFlow(SearchUi()) + + fun runSearch(keyword: String, field: String = search.value.field) { + val trimmed = keyword.trim() + search.update { + it.copy(query = trimmed, field = field, loading = trimmed.isNotBlank(), error = null) + } + if (trimmed.isBlank()) { + search.update { it.copy(hits = emptyList(), loading = false) } + return + } + viewModelScope.launch { + val result = repo.search(trimmed, field) + val hits = result.getOrDefault(emptyList()) + // 输入变化过快时丢弃旧请求的结果 + search.update { ui -> + if (ui.query != trimmed || ui.field != field) { + ui + } else { + ui.copy( + loading = false, + hits = hits, + error = result.exceptionOrNull()?.message + ?: if (hits.isEmpty()) "没有找到歌词" else null, + ) + } + } + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/screen/SubmissionDetailScreen.kt b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SubmissionDetailScreen.kt new file mode 100644 index 0000000..8ffbc90 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/screen/SubmissionDetailScreen.kt @@ -0,0 +1,162 @@ +package dev.amll.hub.android.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.amll.hub.android.ui.AppNav +import dev.amll.hub.android.ui.component.AmllCard +import dev.amll.hub.android.ui.component.AmllTopBar +import dev.amll.hub.android.ui.component.FieldRow +import dev.amll.hub.android.ui.component.GhostButton +import dev.amll.hub.android.ui.component.PrimaryButton +import dev.amll.hub.android.ui.component.StatusChip +import dev.amll.hub.android.ui.theme.Amll +import dev.amll.hub.android.ui.theme.Font + +/* ============================ 09 稿件详情 ============================ */ + +@Composable +fun SubmissionDetailScreen(id: Long, vm: CreatorViewModel, nav: AppNav) { + LaunchedEffect(id) { vm.loadSubmission(id) } + val detail by vm.submissionDetail.collectAsStateWithLifecycle() + var working by remember { mutableStateOf(false) } + val (statusLabel, statusTone) = statusOf(detail?.status.orEmpty()) + val status = detail?.status.orEmpty() + + Column( + Modifier + .fillMaxSize() + .background(Amll.colors.canvas) + .statusBarsPadding() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + AmllTopBar("稿件详情", onBack = nav.back) { + StatusChip(statusLabel, statusTone) + } + + AmllCard(padding = 16.dp) { + // 稿件本身没有封面,这里只展示歌曲信息,不放占位图 + val songTitle = detail?.displayName ?: "未命名稿件" + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text(songTitle, style = Font.bodyMedium, color = Amll.colors.ink, maxLines = 2) + Text( + listOfNotNull( + detail?.artist?.takeIf { it.isNotBlank() }, + detail?.ncmId?.takeIf { it.isNotBlank() }?.let { "网易云 $it" }, + ).joinToString(" · ").ifBlank { "—" }, + style = Font.micro, + color = Amll.colors.ink3, + ) + } + } + + AmllCard(padding = 16.dp) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + FieldRow("稿件编号", detail?.id?.takeIf { it > 0 }?.let { "#$it" } ?: "—") + FieldRow("当前状态", statusLabel) + FieldRow("语言", languageText(detail?.language.orEmpty())) + FieldRow("提交时间", detail?.createdAt?.take(16)?.replace('T', ' ') ?: "—") + FieldRow("最近更新", detail?.updatedAt?.take(16)?.replace('T', ' ') ?: "—") + val fileName = detail?.fileName.orEmpty() + if (fileName.isNotBlank()) FieldRow("文件", fileName) + } + } + + // 审核意见:有才显示,空的不占位 + val comment = detail?.reviewComment.orEmpty() + if (comment.isNotBlank()) { + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(Amll.colors.surface2) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text("审核意见", style = Font.micro.copy(fontWeight = FontWeight.Medium), color = Amll.colors.ink3) + Text(comment, style = Font.bodySmall, color = Amll.colors.ink2) + } + } + + // 修订版待确认:投稿者必须处理的一步 + if (status == "revised") { + AmllCard(padding = 16.dp) { + Text("审核员提交了修订版", style = Font.cardTitle, color = Amll.colors.ink) + Spacer(Modifier.height(4.dp)) + Text( + "采用后按通过处理;退回则需要继续修改。", + style = Font.micro, + color = Amll.colors.ink3, + ) + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + GhostButton( + text = "退回修改", + onClick = { + val id = detail?.id ?: 0L + if (id > 0L && !working) { + working = true + vm.rejectRevision(id) { working = false } + } + }, + modifier = Modifier.weight(1f), + ) + PrimaryButton( + text = "采用修订版", + onClick = { + val id = detail?.id ?: 0L + if (id > 0L && !working) { + working = true + vm.adoptRevision(id) { working = false } + } + }, + modifier = Modifier.weight(1f), + enabled = !working, + ) + } + } + + // 未完结的稿件可以关闭(撤回) + if (status in setOf("draft", "pending", "reviewing", "need_revision", "missing_audio")) { + GhostButton( + text = if (working) "处理中…" else "关闭这篇稿件", + onClick = { + val id = detail?.id ?: 0L + if (id > 0L && !working) { + working = true + vm.closeSubmission(id) { + working = false + nav.back() + } + } + }, + ) + } + } +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Color.kt b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Color.kt index f5ae6a8..6e90bea 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Color.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Color.kt @@ -2,87 +2,89 @@ package dev.amll.hub.android.ui.theme import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat /** - * 配色对齐网页端 index.css 的 --amll-* 设计 token。 - * 深色为默认基调(与网页端 html.dark 一致),浅色跟随系统。 + * 设计系统唯一真源是 [AmllPalette]:M3 的 [ColorScheme] 由它逐项推导, + * 不再单独维护第二套色值,避免同一角色出现两个近似色。 */ - -// 品牌红 -private val AmllPinkLight = Color(0xFFE0303F) -private val AmllPinkDark = Color(0xFFF0424F) -private val AmllPinkHoverLight = Color(0xFFC82836) -private val AmllPinkHoverDark = Color(0xFFFF5763) - -// 浅色主题 -private val LightColors = lightColorScheme( - primary = AmllPinkLight, - onPrimary = Color.White, - primaryContainer = Color(0xFFFBE7E9), - onPrimaryContainer = Color(0xFF8C1621), - secondary = Color(0xFF2A7AB8), - onSecondary = Color.White, - background = Color(0xFFFBFBFD), - onBackground = Color(0xFF1D1D1F), - surface = Color(0xFFFFFFFF), - onSurface = Color(0xFF1D1D1F), - surfaceVariant = Color(0xFFF2F2F5), - onSurfaceVariant = Color(0xFF535359), - outline = Color(0xFFE6E6EB), - outlineVariant = Color(0xFFEDEDF1), - error = Color(0xFFD12D2D), - onError = Color.White, -) - -// 深色主题(网页端 html.dark 的镜像) -private val DarkColors = darkColorScheme( - primary = AmllPinkDark, - onPrimary = Color.White, - primaryContainer = Color(0xFF5C1620), - onPrimaryContainer = Color(0xFFFFD9DC), - secondary = Color(0xFF5AA9E0), - onSecondary = Color(0xFF08243A), - background = Color(0xFF101014), - onBackground = Color(0xFFF5F5F7), - surface = Color(0xFF1A1A1F), - onSurface = Color(0xFFF5F5F7), - surfaceVariant = Color(0xFF26262C), - onSurfaceVariant = Color(0xFF9A9AA2), - outline = Color(0xFF3A3A42), - outlineVariant = Color(0xFF2A2A31), - error = Color(0xFFFF6B6B), - onError = Color(0xFF3A0A0A), -) +private fun AmllPalette.toColorScheme(): ColorScheme { + val scheme = if (isDark) darkColorScheme() else lightColorScheme() + return scheme.copy( + primary = primary, + onPrimary = onPrimary, + primaryContainer = primarySoft, + onPrimaryContainer = primary, + inversePrimary = primary, + secondary = info, + onSecondary = onPrimary, + secondaryContainer = infoSoft, + onSecondaryContainer = info, + tertiary = success, + onTertiary = onPrimary, + tertiaryContainer = successSoft, + onTertiaryContainer = success, + background = canvas, + onBackground = ink, + surface = surface, + onSurface = ink, + surfaceVariant = surface2, + onSurfaceVariant = ink2, + surfaceTint = primary, + inverseSurface = ink, + inverseOnSurface = canvas, + error = error, + onError = onError, + errorContainer = primarySoft, + onErrorContainer = primary, + outline = border, + outlineVariant = border, + scrim = Color.Black, + surfaceBright = surface, + surfaceDim = canvas, + surfaceContainerLowest = if (isDark) canvas else surface, + surfaceContainerLow = if (isDark) surface else surface, + surfaceContainer = surface2, + surfaceContainerHigh = surface2, + surfaceContainerHighest = surface2, + ) +} @Composable fun AmllHubTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { - val colorScheme = if (darkTheme) DarkColors else LightColors + val palette = if (darkTheme) DarkPalette else LightPalette val view = LocalView.current if (!view.isInEditMode) { SideEffect { + // edge-to-edge 下状态栏/导航栏透明,只负责图标明暗;颜色由窗口底色透出 val window = (view.context as Activity).window - window.statusBarColor = colorScheme.background.toArgb() - WindowCompat.getInsetsController(window, view) - .isAppearanceLightStatusBars = !darkTheme + val controller = WindowCompat.getInsetsController(window, view) + controller.isAppearanceLightStatusBars = !darkTheme + controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme( - colorScheme = colorScheme, + colorScheme = palette.toColorScheme(), typography = AmllTypography, - content = content, - ) + shapes = AmllShapes, + ) { + CompositionLocalProvider( + LocalAmllPalette provides palette, + content = content, + ) + } } diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Dimens.kt b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Dimens.kt new file mode 100644 index 0000000..731d956 --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Dimens.kt @@ -0,0 +1,145 @@ +package dev.amll.hub.android.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** 尺寸与字号 token */ +object Space { + val xxs = 4.dp + val xs = 6.dp + val s = 8.dp + val m = 10.dp + val l = 12.dp + val xl = 14.dp + val xxl = 16.dp + val page = 20.dp + val hero = 18.dp +} + +object Radius { + val field = 12.dp + val chip = 14.dp + val card = 18.dp + val hero = 22.dp + val sheet = 24.dp + val pill = 26.dp + val pillOuter = 36.dp +} + +object Size { + val pageWidth = 390.dp + val statusBar = 62.dp + val tabBar = 95.dp + + /** 胶囊导航高度:比原来 62dp 扁一档 */ + val tabPill = 54.dp + val topBar = 32.dp + val coverRow = 48.dp + val coverRank = 40.dp +} + +/** 基础字号 */ +object Font { + private val Sans = FontFamily.SansSerif + + val pageTitle = TextStyle( + fontFamily = Sans, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + lineHeight = 30.sp, + ) + val navTitle = TextStyle( + fontFamily = Sans, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + lineHeight = 24.sp, + ) + val sheetTitle = TextStyle( + fontFamily = Sans, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 22.sp, + ) + val section = TextStyle( + fontFamily = Sans, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 20.sp, + ) + val cardTitle = TextStyle( + fontFamily = Sans, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 18.sp, + ) + val body = TextStyle( + fontFamily = Sans, + fontSize = 13.5.sp, + fontWeight = FontWeight.Normal, + lineHeight = 19.sp, + ) + val bodyMedium = TextStyle( + fontFamily = Sans, + fontSize = 13.5.sp, + fontWeight = FontWeight.Medium, + lineHeight = 18.sp, + ) + val bodySmall = TextStyle( + fontFamily = Sans, + fontSize = 11.5.sp, + fontWeight = FontWeight.Normal, + lineHeight = 16.sp, + ) + val caption = TextStyle( + fontFamily = Sans, + fontSize = 11.sp, + fontWeight = FontWeight.Normal, + lineHeight = 15.sp, + ) + val micro = TextStyle( + fontFamily = Sans, + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + lineHeight = 14.sp, + ) + val tabLabel = TextStyle( + fontFamily = Sans, + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + lineHeight = 13.sp, + ) + val numberL = TextStyle( + fontFamily = Sans, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + lineHeight = 24.sp, + ) + val numberXL = TextStyle( + fontFamily = Sans, + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + lineHeight = 36.sp, + ) +} + +val CardShape = RoundedCornerShape(Radius.card) +val HeroShape = RoundedCornerShape(Radius.hero) +val FieldShape = RoundedCornerShape(Radius.field) +val ChipShape = RoundedCornerShape(Radius.chip) +val SheetShape = RoundedCornerShape(Radius.sheet) +val PillShape = RoundedCornerShape(Radius.pill) +val PillOuterShape = RoundedCornerShape(Radius.pillOuter) + +/** M3 组件的圆角与自绘卡片共用同一套 Radius token,不再落回 M3 默认圆角 */ +val AmllShapes = Shapes( + extraSmall = FieldShape, + small = ChipShape, + medium = CardShape, + large = HeroShape, + extraLarge = SheetShape, +) diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Palette.kt b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Palette.kt new file mode 100644 index 0000000..4a44f7b --- /dev/null +++ b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Palette.kt @@ -0,0 +1,83 @@ +package dev.amll.hub.android.ui.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color + +@Immutable +data class AmllPalette( + val canvas: Color, + val surface: Color, + val surface2: Color, + val ink: Color, + val ink2: Color, + val ink3: Color, + val border: Color, + val primary: Color, + val primarySoft: Color, + val onPrimary: Color, + val success: Color, + val successSoft: Color, + val warning: Color, + val warningSoft: Color, + val info: Color, + val infoSoft: Color, + val error: Color, + val onError: Color, + val isDark: Boolean, +) + +/** 浅色 */ +val LightPalette = AmllPalette( + canvas = Color(0xFFFBFBFD), + surface = Color(0xFFFFFFFF), + surface2 = Color(0xFFF4F4F7), + ink = Color(0xFF1D1D1F), + ink2 = Color(0xFF535359), + ink3 = Color(0xFF8E8E93), + border = Color(0xFFE6E6EB), + primary = Color(0xFFE0303F), + primarySoft = Color(0xFFFDECEE), + onPrimary = Color(0xFFFFFFFF), + success = Color(0xFF3C7A57), + successSoft = Color(0xFFEDF6F0), + warning = Color(0xFFC77A1A), + warningSoft = Color(0xFFFBF1E2), + info = Color(0xFF3B6FA8), + infoSoft = Color(0xFFEDF2F9), + error = Color(0xFFD12D2D), + onError = Color(0xFFFFFFFF), + isDark = false, +) + +/** 深色 */ +val DarkPalette = AmllPalette( + canvas = Color(0xFF101014), + surface = Color(0xFF17171D), + surface2 = Color(0xFF1E1E26), + ink = Color(0xFFF5F5F7), + ink2 = Color(0xFFA1A1AA), + ink3 = Color(0xFF6E6E78), + border = Color(0xFF2A2A34), + primary = Color(0xFFF0424F), + primarySoft = Color(0xFF2A1418), + onPrimary = Color(0xFFFFFFFF), + success = Color(0xFF6FA98A), + successSoft = Color(0xFF16241C), + warning = Color(0xFFE0A055), + warningSoft = Color(0xFF2A2015), + info = Color(0xFF6C9BC9), + infoSoft = Color(0xFF16202B), + error = Color(0xFFFF6B6B), + onError = Color(0xFF3A0A0A), + isDark = true, +) + +val LocalAmllPalette = compositionLocalOf { LightPalette } + +/** 统一的取值入口:Amll.colors.ink2 */ +object Amll { + val colors: AmllPalette + @androidx.compose.runtime.Composable + get() = LocalAmllPalette.current +} diff --git a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Type.kt b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Type.kt index 0b79446..8b22547 100644 --- a/android/app/src/main/java/dev/amll/hub/android/ui/theme/Type.kt +++ b/android/app/src/main/java/dev/amll/hub/android/ui/theme/Type.kt @@ -1,64 +1,25 @@ package dev.amll.hub.android.ui.theme import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp /** - * 字体栈对齐网页端 --amll-font-sans(Inter / PingFang SC / Noto Sans SC / 系统默认)。 - * Android 上用系统 sans-serif 即可覆盖中英混排,无需内嵌字体文件。 + * M3 Typography 只做 [Font] token 的映射,不再维护第二套字号。 + * 这样 M3 组件与自绘组件落在同一档字号上(例如正文统一为 Font.body 13.5sp)。 */ -private val SansFont = FontFamily.SansSerif - val AmllTypography = Typography( - headlineMedium = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.SemiBold, - fontSize = 26.sp, - lineHeight = 32.sp, - ), - titleLarge = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - lineHeight = 26.sp, - ), - titleMedium = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 22.sp, - ), - bodyLarge = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - ), - bodyMedium = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - ), - bodySmall = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - ), - labelLarge = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - ), - labelMedium = TextStyle( - fontFamily = SansFont, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - ), + displayLarge = Font.numberXL, + displayMedium = Font.numberL, + displaySmall = Font.pageTitle, + headlineLarge = Font.pageTitle, + headlineMedium = Font.pageTitle, + headlineSmall = Font.navTitle, + titleLarge = Font.sheetTitle, + titleMedium = Font.section, + titleSmall = Font.cardTitle, + bodyLarge = Font.body, + bodyMedium = Font.body, + bodySmall = Font.bodySmall, + labelLarge = Font.bodyMedium, + labelMedium = Font.caption, + labelSmall = Font.micro, ) diff --git a/android/app/src/main/res/drawable-nodpi/ic_launcher_logo.png b/android/app/src/main/res/drawable-nodpi/ic_launcher_logo.png new file mode 100644 index 0000000..571f178 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/ic_launcher_logo.png differ diff --git a/android/app/src/main/res/drawable/ic_brand.xml b/android/app/src/main/res/drawable/ic_brand.xml new file mode 100644 index 0000000..e6d5193 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_brand.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml index fd7d06a..49e3c1b 100644 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,11 +1,19 @@ - - - - + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_logo_inset.xml b/android/app/src/main/res/drawable/ic_launcher_logo_inset.xml new file mode 100644 index 0000000..fac25e4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_logo_inset.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_stat_music.xml b/android/app/src/main/res/drawable/ic_stat_music.xml new file mode 100644 index 0000000..84b9580 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_stat_music.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 309364f..eeb6248 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml index 8617fed..e120a6d 100644 --- a/android/app/src/main/res/values-night/colors.xml +++ b/android/app/src/main/res/values-night/colors.xml @@ -2,4 +2,6 @@ #101014 + + #F0424F diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index a154261..aa632b2 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -2,4 +2,6 @@ #FBFBFD + + #E0303F diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 322162d..be9d4b5 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ AMLL Hub + + 正在播放 diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 9488b31..b1c22c7 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -1,9 +1,19 @@ - - + + + diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..c9ca485 --- /dev/null +++ b/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml index 3febcf8..09bb277 100644 --- a/android/app/src/main/res/xml/network_security_config.xml +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -1,14 +1,9 @@ - + - - 10.0.2.2 - localhost - 127.0.0.1 - diff --git a/android/app/src/test/java/dev/amll/hub/android/AuthDtoTest.kt b/android/app/src/test/java/dev/amll/hub/android/AuthDtoTest.kt index 43c9faf..e9a28cb 100644 --- a/android/app/src/test/java/dev/amll/hub/android/AuthDtoTest.kt +++ b/android/app/src/test/java/dev/amll/hub/android/AuthDtoTest.kt @@ -1,10 +1,10 @@ package dev.amll.hub.android -import kotlinx.serialization.json.Json import dev.amll.hub.android.data.remote.ApiResponse import dev.amll.hub.android.data.remote.LoginResult import dev.amll.hub.android.data.remote.QrTicketStatus import dev.amll.hub.android.data.remote.SendCodeRequest +import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -38,7 +38,7 @@ class AuthDtoTest { assertNull(pending.token) val confirmed = json.decodeFromString( - """{"status":"confirmed","token":"jwt-456","user":{"name":"bob"}}""" + """{"status":"confirmed","token":"jwt-456","user":{"name":"bob"}}""", ) assertTrue(confirmed.isConfirmed) assertEquals("jwt-456", confirmed.token) @@ -57,7 +57,7 @@ class AuthDtoTest { // 后端 OKWithMsg(c, nil, ...)(发码 / 扫码确认取消)因 `data,omitempty` // 会直接省略 data 字段,客户端不能把 data 缺失当成错误 val resp = json.decodeFromString>( - """{"code":200,"message":"验证码已发送"}""" + """{"code":200,"message":"验证码已发送"}""", ) assertEquals(200, resp.code) assertEquals("验证码已发送", resp.message) diff --git a/android/app/src/test/java/dev/amll/hub/android/ScanTicketParseTest.kt b/android/app/src/test/java/dev/amll/hub/android/ScanTicketParseTest.kt index ae9e164..713e563 100644 --- a/android/app/src/test/java/dev/amll/hub/android/ScanTicketParseTest.kt +++ b/android/app/src/test/java/dev/amll/hub/android/ScanTicketParseTest.kt @@ -1,4 +1,4 @@ -package dev.amll.hub.android.ui.screen +package dev.amll.hub.android.data import org.junit.Assert.assertEquals import org.junit.Test @@ -9,37 +9,24 @@ import org.junit.Test */ class ScanTicketParseTest { - private fun extract(raw: String): String { - val trimmed = raw.trim() - val marker = "ticket=" - val index = trimmed.indexOf(marker) - return if (index >= 0) { - trimmed.substring(index + marker.length).substringBefore('&').trim() - } else { - trimmed - } - } - @Test fun `解析标准 deep link`() { - val ticket = extract("amllhub://qrlogin?ticket=abc123XYZ") - assertEquals("abc123XYZ", ticket) + assertEquals("abc123XYZ", QrTicketParser.parse("amllhub://qrlogin?ticket=abc123XYZ")) } @Test fun `解析带额外查询参数与空格的 deep link`() { - val ticket = extract(" amllhub://qrlogin?ticket=abc123&from=web ") - assertEquals("abc123", ticket) + assertEquals("abc123", QrTicketParser.parse(" amllhub://qrlogin?ticket=abc123&from=web ")) } @Test fun `裸 ticket 也能解析`() { - assertEquals("abc123", extract("abc123")) + assertEquals("abc123", QrTicketParser.parse("abc123")) } @Test fun `空内容解析为空串`() { - assertEquals("", extract("")) - assertEquals("", extract(" ")) + assertEquals("", QrTicketParser.parse("")) + assertEquals("", QrTicketParser.parse(" ")) } } diff --git a/android/app/src/test/java/dev/amll/hub/android/data/remote/ApiErrorParserTest.kt b/android/app/src/test/java/dev/amll/hub/android/data/remote/ApiErrorParserTest.kt new file mode 100644 index 0000000..85ebac9 --- /dev/null +++ b/android/app/src/test/java/dev/amll/hub/android/data/remote/ApiErrorParserTest.kt @@ -0,0 +1,70 @@ +package dev.amll.hub.android.data.remote + +import kotlinx.serialization.SerializationException +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import retrofit2.HttpException +import retrofit2.Response +import java.io.IOException + +/** 网络异常 → 用户可读异常的映射,登录失效与业务错误最容易回归 */ +class ApiErrorParserTest { + + private fun httpException(code: Int, body: String): HttpException = + HttpException(Response.error(code, body.toResponseBody(JSON))) + + @Test + fun `从错误响应体提取后端 message`() { + val e = httpException(400, """{"code":400,"message":"歌名不能为空"}""") + + assertEquals("歌名不能为空", ApiErrorParser.message(e)) + } + + @Test + fun `401 映射为登录失效并保留后端提示`() { + val mapped = ApiErrorParser.map(httpException(401, """{"code":401,"message":"token 已过期"}""")) + + assertTrue(mapped is UnauthorizedException) + assertEquals("token 已过期", mapped.message) + } + + @Test + fun `401 无响应体时使用默认提示`() { + val mapped = ApiErrorParser.map(httpException(401, "")) + + assertTrue(mapped is UnauthorizedException) + assertEquals("登录状态已过期,请重新登录", mapped.message) + } + + @Test + fun `其它 HTTP 错误映射为带状态码的业务异常`() { + val mapped = ApiErrorParser.map(httpException(500, "")) + + assertTrue(mapped is ApiException) + mapped as ApiException + assertEquals(500, mapped.code) + assertEquals("请求失败(HTTP 500)", mapped.message) + } + + @Test + fun `IO 异常映射为网络异常`() { + val mapped = ApiErrorParser.map(IOException("timeout")) + + assertTrue(mapped is NetworkException) + } + + @Test + fun `反序列化异常映射为可读的解析失败`() { + val mapped = ApiErrorParser.map(SerializationException("unexpected json")) + + assertTrue(mapped is IOException) + assertTrue(mapped.message.orEmpty().startsWith("响应解析失败")) + } + + private companion object { + val JSON = "application/json".toMediaType() + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 0da68a8..ab9cec5 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -5,6 +5,9 @@ ksp = "2.3.12" # Hilt 2.60.1 要求 AGP >= 9.0.0(2.59.x 只支持 AGP 8.4+) hilt = "2.60.1" coreKtx = "1.19.1" +# XML 主题(Theme.Material3.DayNight.NoActionBar)来自 Material Components,不是 Compose Material3 +material = "1.13.0" +coreSplashscreen = "1.0.1" lifecycle = "2.11.0" activityCompose = "1.12.4" composeBom = "2026.06.00" @@ -14,29 +17,47 @@ retrofit = "2.12.0" okhttp = "5.5.0" serialization = "1.11.0" coil = "3.6.3" +# 底栏 / 播放条的实时背景模糊(Compose Multiplatform) +haze = "2.0.1" +media3 = "1.5.1" +webkit = "1.12.1" mlkitBarcode = "17.3.0" camerax = "1.5.3" concurrentFutures = "1.3.0" datastore = "1.1.7" coroutines = "1.10.2" +# 下载标签:MP3(mp3agic)/ FLAC(jaudiotagger) +mp3agic = "0.9.1" +jaudiotagger = "3.0.1" +slf4jNop = "2.0.13" junit = "4.13.2" androidxJunit = "1.3.0" espresso = "3.7.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-animation = { group = "androidx.compose.animation", name = "animation" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +# 下载标签:MP3 / FLAC(slf4j-nop 作为日志门面的空实现,避免第三方库找不到 binder) +mp3agic = { group = "com.mpatric", name = "mp3agic", version.ref = "mp3agic" } +jaudiotagger = { group = "net.jthink", name = "jaudiotagger", version.ref = "jaudiotagger" } +slf4j-nop = { group = "org.slf4j", name = "slf4j-nop", version.ref = "slf4jNop" } +androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } @@ -53,6 +74,10 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx- coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } +# 背景模糊:haze 负责采集源,haze-blur 提供模糊效果 +haze-core = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" } +haze-blur = { group = "dev.chrisbanes.haze", name = "haze-blur", version.ref = "haze" } + # 扫码:ML Kit 条码识别 + CameraX 预览 mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcode" } camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } diff --git a/android/lyrics-web/.gitignore b/android/lyrics-web/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/android/lyrics-web/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/android/lyrics-web/index.html b/android/lyrics-web/index.html new file mode 100644 index 0000000..38e1b84 --- /dev/null +++ b/android/lyrics-web/index.html @@ -0,0 +1,12 @@ + + + + + + AMLL Lyrics + + +
+ + + diff --git a/android/lyrics-web/package.json b/android/lyrics-web/package.json new file mode 100644 index 0000000..4872cd1 --- /dev/null +++ b/android/lyrics-web/package.json @@ -0,0 +1,10 @@ +{ + "name": "amll-lyrics-webview", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "沉浸播放页内嵌的 AMLL 歌词页,复用网页端 @applemusic-like-lyrics/react-full,构建产物输出到 android/app/src/main/assets/lyrics", + "scripts": { + "build": "vite build" + } +} diff --git a/android/lyrics-web/pnpm-lock.yaml b/android/lyrics-web/pnpm-lock.yaml new file mode 100644 index 0000000..9b60ae1 --- /dev/null +++ b/android/lyrics-web/pnpm-lock.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} diff --git a/android/lyrics-web/src/main.tsx b/android/lyrics-web/src/main.tsx new file mode 100644 index 0000000..e00e314 --- /dev/null +++ b/android/lyrics-web/src/main.tsx @@ -0,0 +1,464 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Provider, createStore, useAtom } from 'jotai'; +import { + PrebuiltLyricPlayer, + LyricSizePreset, + PlayerControlsType, + cssBackgroundPropertyAtom, + enableLyricRomanLineAtom, + enableLyricTranslationLineAtom, + hideLyricViewAtom, + isLyricPageOpenedAtom, + lyricBackgroundRendererAtom, + lyricSizePresetAtom, + musicArtistsAtom, + musicCoverAtom, + musicDurationAtom, + musicLyricLinesAtom, + musicNameAtom, + musicPlayingAtom, + musicPlayingPositionAtom, + musicVolumeAtom, + onChangeVolumeAtom, + onClickControlThumbAtom, + onCycleRepeatModeAtom, + onLyricLineClickAtom, + onPlayOrResumeAtom, + onRequestNextSongAtom, + onRequestOpenMenuAtom, + onRequestPrevSongAtom, + onSeekPositionAtom, + onToggleShuffleAtom, + playerControlsTypeAtom, + showMusicNameAtom, +} from '@applemusic-like-lyrics/react-full'; +import { MeshGradientRenderer } from '@applemusic-like-lyrics/core'; +// 与网页端 PlayerBoot 完全相同:歌词解析全部交给 AMLL 官方解析器, +// 原生侧只传原始 TTML / LRC 文本,避免两边解析结果不一致(英文空格就是这样丢的) +import { parseLrc, parseTTML } from '@applemusic-like-lyrics/lyric'; +import '@applemusic-like-lyrics/core/style.css'; +import '@applemusic-like-lyrics/react-full/style.css'; +import './page.css'; + +/** + * 与网页端 components/LyricsPage.tsx 相同的渲染层: + * PrebuiltLyricPlayer + jotai atoms,原生侧只负责把 + * 歌词行 / 进度 / 播放态写进 atoms,并把页面上的操作回传原生。 + */ +const store = createStore(); + +// AMLL 的配置 atom 都是 atomWithStorage,会优先读 localStorage 里的旧值。 +// 旧版本页面可能存过 playerControlsType = "none"(大播控里就没有上一首/播放/下一首)、 +// lyricSizePreset = "tiny" 之类的值,升级 APK 后依然生效,看起来像「改了没反应」。 +// 页面启动时先清掉 AMLL 的配置项,保证始终使用下面的默认配置。 +for (let i = localStorage.length - 1; i >= 0; i -= 1) { + const key = localStorage.key(i); + if (key?.startsWith('amll-react-full.')) localStorage.removeItem(key); +} + +store.set(isLyricPageOpenedAtom, true); +store.set(musicPlayingAtom, false); +store.set(musicVolumeAtom, 1); + +/** + * 这些配置 atom 都是 atomWithStorage:挂载时会用 localStorage 里的旧值覆盖默认值。 + * 旧版本页面可能在本地存过别的值(例如播放控制被存成 fft/none,底部就没有播控按钮), + * 所以启动时和挂载后再各写一次,保证与网页端默认一致。 + */ +function applyPrebuiltDefaults() { + // 播放控制组件:controls = 上一首 / 播放暂停 / 下一首 + store.set(playerControlsTypeAtom, PlayerControlsType.Controls); + store.set(showMusicNameAtom, true); + store.set(enableLyricTranslationLineAtom, true); + store.set(enableLyricRomanLineAtom, true); + // 网页端默认档在手机上偏小,这里统一用「大」档(6vh,约比默认大 20%) + store.set(lyricSizePresetAtom, LyricSizePreset.Large); +} +applyPrebuiltDefaults(); + +/** + * 背景默认先走纯色(安全),封面确认能加载后再切回和网页端一致的流体渐变。 + * 封面加载失败时流体渲染器会退化成一片白,白字直接看不见,所以必须先探测。 + */ +let usingMesh = false; +function useMeshBackground() { + if (usingMesh) return; + usingMesh = true; + store.set(lyricBackgroundRendererAtom, { renderer: MeshGradientRenderer }); +} + +function useSolidBackground() { + if (!usingMesh) return; + usingMesh = false; + store.set(lyricBackgroundRendererAtom, { renderer: 'css-bg' }); +} + +store.set(lyricBackgroundRendererAtom, { renderer: 'css-bg' }); +store.set(cssBackgroundPropertyAtom, '#141019'); + +/** 将回调包装为 AMLL 期望的 { onEmit } 结构 */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const toEmit = (onEmit: T): any => ({ onEmit }); + +/** ⋯ 按钮在 AMLL 内部触发,页面内菜单通过这个引用打开 */ +const menuRequest: { listener: (() => void) | null } = { listener: null }; + +/** 页面 → 原生:优先走 JS 接口,接口不可用时退回控制台日志 */ +function host(command: string, value?: number) { + try { + const bridge = (window as unknown as { + amllHost?: { postMessage?: (json: string) => void }; + }).amllHost; + if (bridge && typeof bridge.postMessage === 'function') { + bridge.postMessage(JSON.stringify({ command, value })); + return; + } + } catch (err) { + console.warn('[amll-host] bridge failed', err); + } + console.log('[amll-host]', command, value ?? ''); +} + +// 与网页端一致的交互回调 +store.set(onPlayOrResumeAtom, toEmit(() => host('playPause'))); +store.set(onSeekPositionAtom, toEmit((ms: number) => host('seek', ms))); +store.set( + onLyricLineClickAtom, + toEmit((evt: { line: { getLine: () => { startTime: number } } }) => { + host('seek', evt.line.getLine().startTime); + }), +); +store.set(onChangeVolumeAtom, toEmit((v: number) => host('volume', v))); +store.set(onToggleShuffleAtom, toEmit(() => host('shuffle'))); +store.set(onCycleRepeatModeAtom, toEmit(() => host('repeat'))); +store.set(onRequestNextSongAtom, toEmit(() => host('next'))); +store.set(onRequestPrevSongAtom, toEmit(() => host('prev'))); +// 顶部拖动条 / 点击封面缩略图:网页端用来退出歌词页 +store.set(onClickControlThumbAtom, toEmit(() => host('close'))); +// 右上角「⋯」:与网页端一致,弹出页面内菜单 +store.set( + onRequestOpenMenuAtom, + toEmit(() => { + menuRequest.listener?.(); + }), +); + +/** + * 原生每 120ms 才推一次进度,直接写进 atom 会让逐字高亮一跳一跳的。 + * 这里和网页端的 rAF 循环一样自己做插值,只在偏差超过 250ms 时才校正。 + */ +let basePosition = 0; +let baseTimestamp = performance.now(); +let playing = false; +let lastNativePosition = 0; + +const tick = () => { + if (playing) { + const projected = basePosition + (performance.now() - baseTimestamp); + store.set(musicPlayingPositionAtom, Math.round(projected)); + } + requestAnimationFrame(tick); +}; +requestAnimationFrame(tick); + +function applyPosition(ms: number) { + lastNativePosition = ms; + const now = performance.now(); + if (!playing) { + basePosition = ms; + baseTimestamp = now; + store.set(musicPlayingPositionAtom, ms); + return; + } + const projected = basePosition + (now - baseTimestamp); + if (Math.abs(projected - ms) > 250) { + basePosition = ms; + baseTimestamp = now; + store.set(musicPlayingPositionAtom, ms); + } +} + +const MENU_WIDTH = 208; +const LONG_PRESS_MS = 480; + +type MenuAnchor = { x: number; y: number }; + +function clampMenu(anchor: MenuAnchor): MenuAnchor { + const margin = 8; + const maxX = Math.max(margin, window.innerWidth - MENU_WIDTH - margin); + // 菜单高度约 260px,避免贴着屏幕下沿 + const maxY = Math.max(margin, window.innerHeight - 280); + return { + x: Math.min(Math.max(anchor.x, margin), maxX), + y: Math.min(Math.max(anchor.y, margin), maxY), + }; +} + +function MenuRow(props: { label: string; disabled?: boolean; onClick?: () => void }) { + return ( + + ); +} + +function App() { + const [hideLyric, setHideLyric] = useAtom(hideLyricViewAtom); + const [anchor, setAnchor] = useState(null); + const timerRef = useRef(null); + const startRef = useRef<{ x: number; y: number } | null>(null); + // 菜单刚弹出时手指还没抬起,紧接着的 click 会落在遮罩上,先忽略一小段时间 + const openedAtRef = useRef(0); + + // atomWithStorage 在挂载时会读 localStorage 覆盖默认值,这里在挂载后再纠正一次 + useEffect(() => { + applyPrebuiltDefaults(); + }, []); + + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + const openMenu = useCallback((next: MenuAnchor) => { + openedAtRef.current = Date.now(); + setAnchor(clampMenu(next)); + }, []); + + // AMLL 右上角「⋯」→ 页面内菜单 + useEffect(() => { + menuRequest.listener = () => + openMenu({ x: window.innerWidth - MENU_WIDTH - 16, y: 64 }); + return () => { + menuRequest.listener = null; + }; + }, [openMenu]); + + const onTouchStart = (e: React.TouchEvent) => { + if (e.touches.length !== 1) return; + const touch = e.touches[0]; + startRef.current = { x: touch.clientX, y: touch.clientY }; + clearTimer(); + timerRef.current = window.setTimeout(() => { + timerRef.current = null; + openMenu({ x: touch.clientX, y: touch.clientY }); + }, LONG_PRESS_MS); + }; + + const onTouchMove = (e: React.TouchEvent) => { + const start = startRef.current; + if (!start) return; + const touch = e.touches[0]; + if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) { + clearTimer(); + } + }; + + const closeMenu = () => setAnchor(null); + + return ( +
{ + // 安卓 WebView 长按默认会弹系统菜单,这里统一走页面内菜单 + e.preventDefault(); + openMenu({ x: e.clientX, y: e.clientY }); + }} + > + + + {anchor && ( +
{ + if (Date.now() - openedAtRef.current > 300) closeMenu(); + }} + > +
e.stopPropagation()} + > + + { + closeMenu(); + host('playPause'); + }} + /> + +
+ { + setHideLyric(!hideLyric); + closeMenu(); + }} + /> +
+ { + closeMenu(); + host('close'); + }} + /> +
+
+ )} +
+ ); +} + +type NativeBridge = { + /** 原始歌词文本:format = "ttml" | "lrc",translation 只对 LRC 有效 */ + setRawLyric: (raw: string, format: string, translation: string) => void; + setTime: (ms: number) => void; + setPlaying: (playing: boolean) => void; + setMeta: (name: string, artists: string, duration: number) => void; + setCover: (url: string) => void; + setTheme: (dark: boolean) => void; + /** 状态栏 / 导航栏高度(CSS px),用于给内容留出安全区,背景仍然铺满 */ + setInsets: (top: number, bottom: number) => void; +}; + +declare global { + interface Window { + amllNative: NativeBridge; + } +} + +window.amllNative = { + setRawLyric(raw: string, format: string, translation: string) { + try { + if (!raw) { + store.set(musicLyricLinesAtom, []); + return; + } + // 与 PlayerBoot.tsx L1294-1331 一致:TTML 走 parseTTML, + // LRC 走 parseLrc 并按行开始时间合并 tlyric 翻译 + if (format === 'ttml') { + const lines = parseTTML(raw).lines; + console.log('[amll] parseTTML lines:', lines.length); + store.set(musicLyricLinesAtom, lines); + return; + } + const lines = parseLrc(raw); + if (translation) { + const transMap = new Map(); + for (const t of parseLrc(translation)) { + transMap.set(Math.round(t.startTime), t.words[0]?.word ?? ''); + } + for (const line of lines) { + line.translatedLyric = transMap.get(Math.round(line.startTime)) ?? ''; + } + } + console.log('[amll] parseLrc lines:', lines.length); + store.set(musicLyricLinesAtom, lines); + } catch (err) { + console.warn('[amll] setRawLyric failed', err); + store.set(musicLyricLinesAtom, []); + } + }, + setTime(ms: number) { + applyPosition(ms); + }, + setPlaying(next: boolean) { + console.log('[amll] setPlaying:', next); + playing = next; + if (next) { + basePosition = lastNativePosition; + baseTimestamp = performance.now(); + } + store.set(musicPlayingAtom, next); + }, + setMeta(name: string, artists: string, duration: number) { + console.log('[amll] setMeta:', name, artists, duration); + store.set(musicNameAtom, name); + store.set( + musicArtistsAtom, + artists + .split(' / ') + .map((n) => n.trim()) + .filter(Boolean) + .map((n) => ({ id: n, name: n })), + ); + store.set(musicDurationAtom, duration); + }, + // 封面驱动背景流体渐变(与网页端一致);加载失败时退回纯色,保证歌词可读 + setCover(url: string) { + console.log('[amll] setCover:', url); + const cover = url || ''; + store.set(musicCoverAtom, cover); + if (!cover) { + useSolidBackground(); + return; + } + const probe = new Image(); + probe.crossOrigin = 'anonymous'; + probe.onload = () => useMeshBackground(); + probe.onerror = () => useSolidBackground(); + probe.src = cover; + }, + setTheme(dark: boolean) { + document.documentElement.classList.toggle('dark', dark); + document.body.style.background = dark ? '#0B0B0F' : '#FBFBFD'; + }, + // edge-to-edge 下 WebView 会铺到状态栏/导航栏后面,原生把两个高度传进来, + // 页面只把内容往内缩(背景渐变仍然满屏,状态栏不会出现一条黑带) + setInsets(top: number, bottom: number) { + const root = document.documentElement; + root.style.setProperty('--amll-inset-top', `${Math.max(0, top)}px`); + root.style.setProperty('--amll-inset-bottom', `${Math.max(0, bottom)}px`); + }, +}; + +/** AMLL 渲染抛异常时把堆栈画出来,避免整页黑屏且无从排查 */ +class PageErrorBoundary extends React.Component< + { children: React.ReactNode }, + { error: Error | null } +> { + state: { error: Error | null } = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('[amll] render crashed:', error, info.componentStack); + } + + render() { + const { error } = this.state; + if (error) { + return ( +
+
歌词页渲染失败
+
{String(error.stack || error)}
+
+ ); + } + return this.props.children; + } +} + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/android/lyrics-web/src/page.css b/android/lyrics-web/src/page.css new file mode 100644 index 0000000..c37f788 --- /dev/null +++ b/android/lyrics-web/src/page.css @@ -0,0 +1,160 @@ +html, +body, +#root { + height: 100%; + margin: 0; + padding: 0; + background: #0b0b0f; + color: #fff; + overflow: hidden; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: 'Noto Sans SC', -apple-system, 'PingFang SC', system-ui, sans-serif; +} + +/* + * WebView 里 html/body/#root 的百分比高度不可靠(真机上实测 #amll-lyric-player + * 高度为 0),这里直接按视口固定定位,让尺寸不依赖父级百分比。 + * align-items 用 stretch,即便子元素 height:100% 失效也能撑满。 + */ +.stage { + position: fixed; + inset: 0; + display: flex; + align-items: stretch; + justify-content: center; +} + +.stage > #amll-lyric-player { + position: relative; + z-index: 1; +} + +/* 容器本身不加内边距:流体渐变背景画在容器内部,一旦给容器 padding, + * 状态栏/导航栏那一条就会露出页面底色,看起来像两条黑带。 + * 内边距放到 AMLL 布局根节点上(见下方规则),背景仍然铺满整屏。 */ +#amll-lyric-player { + box-sizing: border-box; + padding: 0; +} + +/* 安全区:状态栏/导航栏高度由原生通过 setInsets 写进 --amll-inset-*。 + * 只内缩布局内容,不影响背景槽(背景槽是布局根节点的兄弟节点)。 + * 8px 是原本就有的页面留白。 */ +#amll-lyric-player [class*="horizontalLayout"], +#amll-lyric-player [class*="verticalLayout"] { + box-sizing: border-box; + padding: calc(var(--amll-inset-top, 0px) + 8px) 8px var(--amll-inset-bottom, 0px); +} + +/* 竖屏歌词字号。 + * AMLL 的预设是全局的(横屏按 6vh 算出的字号在竖屏会达到 50px,长句折成三行; + * 换小一档又会让横屏偏小)。这里只在竖屏按宽度定档,横屏继续用 AMLL 预设。 */ +@media (orientation: portrait) { + #amll-lyric-player .amll-lyric-player { + font-size: max(9.5vw, 30px) !important; + } +} + +/* 手机横屏高度很矮时,AMLL 默认 45vh 的封面会把左侧信息栏挤成 0 高 + * (歌名/歌手、进度、播控、音量全被压没,只剩歌词)。 + * 缩小封面,并让信息/播控不参与收缩,把左侧一列完整保留。 */ +@media (orientation: landscape) and (max-height: 640px) { + #amll-lyric-player [class*="horizontalLayout"] { + /* 封面同时受高度和宽度约束,并给下方「歌名/进度/播控/音量」留出空间, + * 否则在 16:9 左右的手机横屏上封面会直接把信息栏挤出屏幕。 */ + --horizontal-layout-max-width: min(36vh, 34vw, calc(100vh - 240px)); + } + + #amll-lyric-player [class*="musicInfo"], + #amll-lyric-player [class*="horizontalControls"], + #amll-lyric-player [class*="controls"], + #amll-lyric-player [class*="volumeControl"] { + flex-shrink: 0; + } +} + +#amll-lyric-player ::-webkit-scrollbar { + display: none; +} + +/* 长按弹出菜单,避免触发系统选择/长按菜单 */ +.stage { + -webkit-touch-callout: none; + user-select: none; +} + +.menu-backdrop { + position: fixed; + inset: 0; + z-index: 1000; +} + +/* 与网页端右键菜单同款:白色圆角卡片 + 深色文字 */ +.menu-card { + position: fixed; + box-sizing: border-box; + padding: 6px; + border-radius: 12px; + background: #fff; + color: #1c1c1e; + box-shadow: 0 14px 40px -12px rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; +} + +.menu-item { + appearance: none; + border: 0; + background: transparent; + color: inherit; + font-family: inherit; + font-size: 14px; + line-height: 1.2; + text-align: left; + padding: 10px 12px; + border-radius: 6px; + cursor: default; +} + +.menu-item:active { + background: #ededf0; +} + +.menu-item:disabled { + color: #b0b0b6; +} + +.menu-sep { + height: 1px; + margin: 4px 8px; + background: rgba(0, 0, 0, 0.1); +} + +/* 渲染异常时的可见兜底,避免纯黑屏 */ +.page-error { + position: fixed; + inset: 0; + padding: 16px; + overflow: auto; + box-sizing: border-box; + background: #300018; + color: #fff; + font-size: 12px; + white-space: pre-wrap; +} + +.page-error-title { + color: #ffb4ab; + font-weight: 600; + margin-bottom: 8px; +} + +.page-error pre { + margin: 0; + font-size: 11px; + white-space: pre-wrap; + word-break: break-all; +} diff --git a/android/lyrics-web/vite.config.ts b/android/lyrics-web/vite.config.ts new file mode 100644 index 0000000..df362d5 --- /dev/null +++ b/android/lyrics-web/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath, URL } from 'node:url'; + +/** + * 构建产物直接落到 Android assets,APK 内离线可用。 + * base: './' 让 file:///android_asset/lyrics/index.html 能正确找到相对资源。 + */ +export default defineConfig({ + base: './', + plugins: [react()], + build: { + outDir: fileURLToPath(new URL('../app/src/main/assets/lyrics', import.meta.url)), + emptyOutDir: true, + assetsInlineLimit: 0, + chunkSizeWarningLimit: 2048, + }, +}); diff --git a/frontend/src/components/LyricsPage.tsx b/frontend/src/components/LyricsPage.tsx index 9670b8e..304be32 100644 --- a/frontend/src/components/LyricsPage.tsx +++ b/frontend/src/components/LyricsPage.tsx @@ -1,5 +1,6 @@ import { AudioQualityType, + hideLyricViewAtom, isLyricPageOpenedAtom, musicArtistsAtom, musicCoverAtom, @@ -27,8 +28,17 @@ import { import '@applemusic-like-lyrics/core/style.css'; import '@applemusic-like-lyrics/react-full/style.css'; import type { LyricLineMouseEvent } from '@applemusic-like-lyrics/core'; -import { useAtomValue, useStore } from 'jotai'; -import { type FC, useEffect, useRef } from 'react'; +import { useAtom, useAtomValue, useStore } from 'jotai'; +import { type FC, useCallback, useEffect, useRef, useState } from 'react'; +import { lyricErrorAtom } from '@/atoms/player'; +import { playerActions } from '@/boot/PlayerBoot'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@/components/ui/context-menu'; import { NCM_QUALITY_LABEL, usePlayer, type NcmQuality } from '@/hooks/usePlayer'; /** 将回调包装为 AMLL 期望的 { onEmit } 结构 */ @@ -258,45 +268,137 @@ const PlayerStateSync: FC = () => { return null; }; +/** 右键菜单行:左侧文案,右侧灰字快捷键提示 */ +const MenuRow: FC<{ label: string; shortcut?: string; onSelect: () => void }> = ({ + label, + shortcut, + onSelect, +}) => ( + + {label} + {shortcut && ( + {shortcut} + )} + +); + /** 歌词页面 */ export function LyricsPage() { const isLyricPageOpened = useAtomValue(isLyricPageOpenedAtom); + const [hideLyricView, setHideLyricView] = useAtom(hideLyricViewAtom); + const lyricError = useAtomValue(lyricErrorAtom); const store = useStore(); - const { lyricError } = usePlayer(); + const [menuOpen, setMenuOpen] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + + // 全屏状态跟随浏览器(F11 / Esc 退出全屏都能同步) + useEffect(() => { + const sync = () => setIsFullscreen(Boolean(document.fullscreenElement)); + sync(); + document.addEventListener('fullscreenchange', sync); + return () => document.removeEventListener('fullscreenchange', sync); + }, []); + + const toggleFullscreen = useCallback(() => { + const action = document.fullscreenElement + ? document.exitFullscreen() + : document.documentElement.requestFullscreen(); + void action.catch(() => { + // 浏览器拒绝(非用户手势等)时保持原状 + }); + }, []); + + // 关闭歌词页(全屏态一并退出,避免留在全屏的黑屏上) + const closeLyricPage = useCallback(() => { + if (document.fullscreenElement) { + void document.exitFullscreen().catch(() => {}); + } + store.set(isLyricPageOpenedAtom, false); + }, [store]); - // Esc 关闭歌词页 + // Esc 关闭歌词页;Ctrl Alt ←/P/→ 上一首/暂停/下一首 + // F11 交给浏览器原生全屏,Web 端无法拦截也不该拦截,只在菜单里作为提示 useEffect(() => { if (!isLyricPageOpened) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { + // 菜单打开时 Esc 只关菜单 + if (menuOpen) return; e.preventDefault(); - store.set(isLyricPageOpenedAtom, false); + closeLyricPage(); + return; + } + if (!e.ctrlKey || !e.altKey) return; + const key = e.key.toLowerCase(); + if (key === 'arrowleft') { + e.preventDefault(); + playerActions.playPrev(); + } else if (key === 'arrowright') { + e.preventDefault(); + playerActions.playNext(); + } else if (key === 'p') { + e.preventDefault(); + playerActions.toggle(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [isLyricPageOpened, store]); + }, [isLyricPageOpened, menuOpen, closeLyricPage]); return ( <> -
- {/* 错误提示 */} - {lyricError && ( -
- {lyricError} + + +
+ {/* 错误提示 */} + {lyricError && ( +
+ {lyricError} +
+ )} + + {/* AMLL 播放器 */} +
- )} +
+ + {/* 歌词页右键菜单 */} + + playerActions.playPrev()} /> + playerActions.toggle()} + /> + playerActions.playNext()} /> + + + + setHideLyricView(!hideLyricView)} + /> + + + - {/* AMLL 播放器 */} - -
+ + + ); } diff --git a/frontend/src/hooks/useNcmParse.ts b/frontend/src/hooks/useNcmParse.ts index 5c741d6..8d14b8f 100644 --- a/frontend/src/hooks/useNcmParse.ts +++ b/frontend/src/hooks/useNcmParse.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { useQuery } from '@tanstack/react-query'; import { api } from '@/lib/api'; +import { extractNcmSongId } from '@/lib/ncm'; import { queryKeys } from '@/lib/query'; import type { NcmPlaylistDetail, NcmSong } from '@/lib/types'; import { qualityAtom, type NcmQuality } from '@/atoms/player'; @@ -115,17 +116,19 @@ export function useNcmParse(): NcmParseContextValue { songId: string, meta: { name?: string; artists?: string; cover?: string; duration?: number } ) => { + // 分享链接先归一成纯 ID + const id = extractNcmSongId(songId); setParseLoading(true); setParseError(null); setHideSearchList(true); - setSongIdInput(songId); + setSongIdInput(id); // 真正解析时才把页面选择同步到全局,播放器与歌词页的音质随之对齐 setGlobalQuality(quality); try { - const info = await api.parseNcmMusic(songId, quality); + const info = await api.parseNcmMusic(id, quality); // 先填充播放信息,歌词异步获取(避免阻塞播放按钮) setParsedSong({ - songId, + songId: id, info, metaDuration: meta.duration, lyricLines: null, @@ -136,7 +139,7 @@ export function useNcmParse(): NcmParseContextValue { try { const { parseLrc, parseTTML } = await import('@applemusic-like-lyrics/lyric'); // ① ncm-lyrics/{songId} TTML - const ttml = await api.getNcmLyricTtml(songId); + const ttml = await api.getNcmLyricTtml(id); if (ttml) { setParsedSong((prev) => (prev ? { ...prev, lyricLines: parseTTML(ttml).lines } : prev)); return; diff --git a/frontend/src/lib/__tests__/ncm.test.ts b/frontend/src/lib/__tests__/ncm.test.ts new file mode 100644 index 0000000..ec081d8 --- /dev/null +++ b/frontend/src/lib/__tests__/ncm.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { extractNcmSongId } from '@/lib/ncm'; + +describe('extractNcmSongId', () => { + it('纯数字原样返回(去首尾空格)', () => { + expect(extractNcmSongId(' 1234567 ')).toBe('1234567'); + }); + + it('从 ?id= / #/song?id= 中提取', () => { + expect(extractNcmSongId('https://music.163.com/song?id=1234567&userid=1')).toBe('1234567'); + expect(extractNcmSongId('https://music.163.com/#/song?id=1234567')).toBe('1234567'); + expect(extractNcmSongId('https://y.music.163.com/m/song?id=1234567&uct2=abc')).toBe('1234567'); + }); + + it('从 /song/{id} 路径中提取', () => { + expect(extractNcmSongId('https://music.163.com/song/1234567')).toBe('1234567'); + }); + + it('分享文案里夹带的链接也能提取', () => { + expect( + extractNcmSongId('分享单曲 https://y.music.163.com/m/song?id=1234567 (@网易云音乐)') + ).toBe('1234567'); + }); + + it('认不出时原样返回', () => { + expect(extractNcmSongId('hello')).toBe('hello'); + expect(extractNcmSongId('https://example.com/?id=123')).toBe('https://example.com/?id=123'); + }); +}); diff --git a/frontend/src/lib/motion.ts b/frontend/src/lib/motion.ts index 745dd50..072d8e0 100644 --- a/frontend/src/lib/motion.ts +++ b/frontend/src/lib/motion.ts @@ -1,6 +1,6 @@ import type { Transition, Variants } from 'framer-motion'; -// 与设计稿一致的缓动 cubic-bezier(.2,.8,.2,1) +// 缓动 cubic-bezier(.2,.8,.2,1) const easeOut: Transition = { duration: 0.5, ease: [0.2, 0.8, 0.2, 1] }; // 淡入 + 上移 diff --git a/frontend/src/lib/ncm.ts b/frontend/src/lib/ncm.ts new file mode 100644 index 0000000..7abf819 --- /dev/null +++ b/frontend/src/lib/ncm.ts @@ -0,0 +1,8 @@ +export function extractNcmSongId(input: string): string { + const value = input.trim(); + if (/^\d+$/.test(value)) return value; + // 只处理网易云链接,别把普通文本里的 id= 误当成歌曲 ID + if (!value.includes('music.163.com')) return value; + const matched = /[?&#]id=(\d+)/.exec(value) ?? /\/song\/(\d+)/.exec(value); + return matched?.[1] ?? value; +}