Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
692801e
feat: 完成 Android 原生应用核心功能实现
cybaka520 Oct 21, 2026
5db63fc
refactor: 简化图标引用方式并修复子项点击逻辑
cybaka520 Oct 21, 2026
449d747
feat: 为 AppModule 添加 ContentApi 和 RawApi 的依赖提供
cybaka520 Oct 21, 2026
9e48985
feat: 添加人机验证、最新收录页与迷你播放器等新功能
cybaka520 Oct 22, 2026
cb2f3a2
feat(ui): 添加垂直滚动功能到主屏幕
cybaka520 Oct 22, 2026
393e529
style: 调整部分文件的导入顺序
cybaka520 Oct 22, 2026
5beb3f5
feat: 新增解析功能与播放界面优化
cybaka520 Oct 23, 2026
90352f0
refactor: 移除废弃的ncmSearch方法并修复Uri类型推断问题
cybaka520 Oct 23, 2026
b419c9d
fix: 为 PlaybackService 添加 UnstableApi 注解解决 lint 错误
cybaka520 Oct 23, 2026
438b64c
feat: 重构扫码页与底栏,修复崩溃问题
cybaka520 Oct 23, 2026
1715f7c
feat: 优化 Android 端首页、导航栏、扫码及崩溃日志功能
cybaka520 Oct 23, 2026
30e0a37
fix: 修复播放器崩溃问题
cybaka520 Oct 23, 2026
860bdd8
feat: 支持音乐域名和封面背景
cybaka520 Oct 23, 2026
3d2e90f
fix: 修复播放按钮在解析失败时无反应且显示错误状态
cybaka520 Oct 23, 2026
236d727
fix: 优化歌曲解析失败的错误提示,展示具体原因
cybaka520 Oct 23, 2026
f41decb
fix: 修复网易云接口数字字段类型不稳定的问题
cybaka520 Oct 23, 2026
6d6dda1
feat: 为歌词页添加调试日志与 WebView 诊断功能
cybaka520 Oct 23, 2026
0ba94ee
fix: 修正 WebView 日志中消息等级的字段名
cybaka520 Oct 23, 2026
2f8b532
feat: 歌词页渲染异常时显示错误兜底界面并写入调试日志
cybaka520 Oct 23, 2026
98143fa
fix(lyrics): 修复 WebView 歌词页黑屏问题
cybaka520 Oct 23, 2026
1302b8b
fix(lyric): 修复 WebView 歌词容器高度问题及加载态显示
cybaka520 Oct 24, 2026
b368cbb
refactor: 删除歌词页调试相关代码和资源
cybaka520 Oct 24, 2026
1e230ce
feat: 重构应用架构,新增多视图模型与导航功能
cybaka520 Oct 24, 2026
43f6adb
feat: 新增 SectionHeader 组件并在 LyricScreen 启用 Material3 实验性 API
cybaka520 Oct 24, 2026
2c7e29d
fix: 修复歌词时间解析错误及搜索页协程配置问题
cybaka520 Oct 24, 2026
ed6c512
refactor: 整理代码结构,删除冗余导入和空行
cybaka520 Oct 24, 2026
94a4be4
refactor: 调整条件判断的格式以提高可读性
cybaka520 Oct 24, 2026
15b3f61
feat: 新增 keystore 配置和搜索胶囊组件,优化认证与播放器逻辑
cybaka520 Oct 24, 2026
b2d1c18
refactor: 歌词解析逻辑统一交由 AMLL 官方库处理
cybaka520 Oct 24, 2026
a264de6
style: 删除多余空行
cybaka520 Oct 24, 2026
834534e
feat: 新增歌词与每日推荐投稿功能,优化网络与界面适配
cybaka520 Oct 24, 2026
08dec7f
fix: 修复文件大小计算为空时返回0的问题,调整DotSeparator颜色获取时机
cybaka520 Oct 24, 2026
b910782
refactor: 清理无用导入并添加 LyricPageState 注释
cybaka520 Oct 24, 2026
01554ad
feat(lyrics): 实现沉浸模式边到边适配,修复状态栏黑带问题
cybaka520 Oct 25, 2026
325799d
feat: 实现歌曲下载功能及相关UI组件
cybaka520 Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ pnpm-debug.log*
*.pem
*.key
*private-key*
*.p12
*.jks
*.keystore
/android/keystore
9 changes: 9 additions & 0 deletions android/.editorconfig
Original file line number Diff line number Diff line change
@@ -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
60 changes: 50 additions & 10 deletions android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)

75 changes: 61 additions & 14 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 的运行时行为变更
Expand All @@ -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")
}
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions android/app/src/debug/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 仅 debug 允许明文访问本机后端 -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>
33 changes: 31 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 扫码需要相机 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 后台播放:前台服务 + 媒体通知 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 后台播放时保持 CPU / 网络唤醒,避免息屏后被系统挂起 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 歌曲下载:前台 dataSync 服务;API 28 及以下写公共下载目录 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<application
android:name=".AmllHubApp"
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
Expand All @@ -23,7 +36,7 @@
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.AmllHub">
android:theme="@style/Theme.AmllHub.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand All @@ -37,5 +50,21 @@
<data android:scheme="amllhub" android:host="qrlogin" />
</intent-filter>
</activity>

<!-- 后台播放:Media3 默认通知栏与锁屏控制 -->
<service
android:name=".playback.PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>

<!-- 网易云单曲 / 歌单批量下载 -->
<service
android:name=".download.DownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
Loading
Loading