Skip to content

Commit d5f6700

Browse files
committed
feat(lyric): 添加媒体通知歌词并添加更多设置
1 parent be5719c commit d5f6700

28 files changed

Lines changed: 4432 additions & 1665 deletions

app/src/main/java/org/parallel_sekai/kanade/MainActivity.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.parallel_sekai.kanade
33
import android.Manifest
44
import android.app.Activity
55
import android.content.Intent
6-
import android.graphics.Color as AndroidColor
76
import android.os.Build
87
import android.os.Bundle
98
import androidx.activity.ComponentActivity
@@ -43,6 +42,8 @@ import org.parallel_sekai.kanade.data.repository.PlaybackRepository
4342
import org.parallel_sekai.kanade.data.repository.SettingsRepository
4443
import org.parallel_sekai.kanade.data.utils.CacheManager
4544
import org.parallel_sekai.kanade.data.utils.LyricGetterManager
45+
import org.parallel_sekai.kanade.ui.adaptive.adaptiveContentWidth
46+
import org.parallel_sekai.kanade.ui.adaptive.rememberAdaptiveLayoutInfo
4647
import org.parallel_sekai.kanade.ui.screens.artist.ArtistDetailScreen
4748
import org.parallel_sekai.kanade.ui.screens.artist.ArtistListScreen
4849
import org.parallel_sekai.kanade.ui.screens.library.AlbumDetailScreen
@@ -65,15 +66,15 @@ import org.parallel_sekai.kanade.ui.screens.settings.CacheSettingsScreen
6566
import org.parallel_sekai.kanade.ui.screens.settings.ExcludedFoldersScreen
6667
import org.parallel_sekai.kanade.ui.screens.settings.LyricsGetterApiScreen
6768
import org.parallel_sekai.kanade.ui.screens.settings.LyricsSettingsScreen
69+
import org.parallel_sekai.kanade.ui.screens.settings.MediaNotificationLyricsScreen
6870
import org.parallel_sekai.kanade.ui.screens.settings.ScriptConfigScreen
6971
import org.parallel_sekai.kanade.ui.screens.settings.ScriptManagementScreen
7072
import org.parallel_sekai.kanade.ui.screens.settings.SettingsScreen
7173
import org.parallel_sekai.kanade.ui.screens.settings.SettingsViewModel
72-
import org.parallel_sekai.kanade.ui.adaptive.adaptiveContentWidth
73-
import org.parallel_sekai.kanade.ui.adaptive.rememberAdaptiveLayoutInfo
7474
import org.parallel_sekai.kanade.ui.screens.settings.SuperLyricApiScreen
7575
import org.parallel_sekai.kanade.ui.theme.Dimens
7676
import org.parallel_sekai.kanade.ui.theme.KanadeTheme
77+
import android.graphics.Color as AndroidColor
7778

7879
sealed class Screen(
7980
val route: String,
@@ -94,6 +95,8 @@ sealed class Screen(
9495

9596
object SuperLyricApi : Screen("super_lyric_api", R.string.pref_super_lyric, null)
9697

98+
object MediaNotificationLyrics : Screen("media_notification_lyrics", R.string.title_media_notification_lyrics, null)
99+
97100
object ExcludedFolders : Screen("excluded_folders", R.string.title_excluded_folders, null)
98101

99102
object CacheSettings : Screen("cache_settings", R.string.title_cache_settings, null)
@@ -498,6 +501,11 @@ class MainActivity : ComponentActivity() {
498501
)
499502
},
500503
onNavigateToSuperLyricApi = { navController.navigate(Screen.SuperLyricApi.route) },
504+
onNavigateToMediaNotificationLyrics = {
505+
navController.navigate(
506+
Screen.MediaNotificationLyrics.route,
507+
)
508+
},
501509
)
502510
}
503511
composable(Screen.LyricsSettings.route) {
@@ -518,6 +526,12 @@ class MainActivity : ComponentActivity() {
518526
onNavigateBack = { navController.popBackStack() },
519527
)
520528
}
529+
composable(Screen.MediaNotificationLyrics.route) {
530+
MediaNotificationLyricsScreen(
531+
viewModel = settingsViewModel,
532+
onNavigateBack = { navController.popBackStack() },
533+
)
534+
}
521535
composable(Screen.ExcludedFolders.route) {
522536
ExcludedFoldersScreen(
523537
viewModel = settingsViewModel,
@@ -596,7 +610,14 @@ class MainActivity : ComponentActivity() {
596610
items.forEach { screen ->
597611
val label = stringResource(screen.labelResId)
598612
NavigationRailItem(
599-
icon = { screen.icon?.let { Icon(it, contentDescription = label) } },
613+
icon = {
614+
screen.icon?.let {
615+
Icon(
616+
it,
617+
contentDescription = label,
618+
)
619+
}
620+
},
600621
label = { Text(label) },
601622
alwaysShowLabel = true,
602623
selected = currentRoute == screen.route,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.parallel_sekai.kanade.data.model
2+
3+
/**
4+
* 外部歌词 API 设置(用于 LyricGetter 和 SuperLyric)
5+
*/
6+
data class ExternalLyricApiSettings(
7+
/** 是否启用该 API 的歌词发送 */
8+
val enabled: Boolean = true,
9+
/** 是否启用滚动截取长文本 */
10+
val scrollingTruncateEnabled: Boolean = false,
11+
/** 最大显示单位数(用于截取) */
12+
val maxDisplayUnits: Int = 40,
13+
/** 是否启用智能单位计算(中日韩/全角=1,ASCII/半角=0.5) */
14+
val smartUnitsEnabled: Boolean = true,
15+
/** 是否显示时刻 */
16+
val showTimestamp: Boolean = false,
17+
/** 允许显示的设备状态:0=解锁,1=亮屏未解锁,2=熄屏 */
18+
val displayStates: Set<Int> = setOf(0, 1, 2),
19+
/** 暂停时是否自动清除歌词 */
20+
val clearOnPause: Boolean = true,
21+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.parallel_sekai.kanade.data.model
2+
3+
/**
4+
* 媒体通知歌词设置
5+
*/
6+
data class MediaNotificationLyricsSettings(
7+
/** 是否启用媒体通知歌词 */
8+
val enabled: Boolean = false,
9+
/** 显示模式:0=模式A(title=原文,artist=歌曲名+艺术家),1=模式B(title=原文,artist=翻译) */
10+
val mode: Int = 0,
11+
/** 是否启用滚动截取长文本 */
12+
val scrollingTruncateEnabled: Boolean = false,
13+
/** 最大显示单位数(用于截取) */
14+
val maxDisplayUnits: Int = 40,
15+
/** 是否启用智能单位计算(中日韩/全角=1,ASCII/半角=0.5) */
16+
val smartUnitsEnabled: Boolean = true,
17+
/** 是否显示时刻 */
18+
val showTimestamp: Boolean = false,
19+
/** 允许显示的设备状态:0=解锁,1=亮屏未解锁,2=熄屏 */
20+
val displayStates: Set<Int> = setOf(0, 1, 2),
21+
/** 暂停时是否还原通知 */
22+
val restoreOnPause: Boolean = true,
23+
/** 翻译最大显示单位数(用于截取翻译文本) */
24+
val translationMaxDisplayUnits: Int = 40,
25+
)

app/src/main/java/org/parallel_sekai/kanade/data/repository/PlaybackRepository.kt

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.parallel_sekai.kanade.data.source.IMusicSource
2020
import org.parallel_sekai.kanade.data.source.MusicListResult
2121
import org.parallel_sekai.kanade.data.source.SourceManager
2222
import org.parallel_sekai.kanade.data.source.local.LocalMusicSource
23+
import org.parallel_sekai.kanade.data.utils.MediaNotificationLyricsState
2324
import org.parallel_sekai.kanade.service.KanadePlaybackService
2425

2526
/**
@@ -194,13 +195,17 @@ open class PlaybackRepository(
194195
val playlist =
195196
(0 until ctrl.mediaItemCount).map { i ->
196197
val item = ctrl.getMediaItemAt(i)
198+
// 使用 extras 中保存的原始信息,避免歌词污染
199+
val originalTitle =
200+
item.mediaMetadata.extras?.getString("original_title") ?: item.mediaMetadata.title?.toString() ?: ""
201+
val originalArtists =
202+
item.mediaMetadata.extras?.getString("original_artists") ?: item.mediaMetadata.artist?.toString()
203+
?: ""
204+
197205
MusicModel(
198206
id = item.mediaMetadata.extras?.getString("original_id") ?: item.mediaId,
199-
title = item.mediaMetadata.title?.toString() ?: "",
200-
artists =
201-
item.mediaMetadata.artist
202-
?.toString()
203-
?.split(_artistJoinString.value) ?: emptyList(),
207+
title = originalTitle,
208+
artists = originalArtists.split(_artistJoinString.value),
204209
album = item.mediaMetadata.albumTitle?.toString() ?: "",
205210
coverUrl = item.mediaMetadata.artworkUri?.toString() ?: "",
206211
mediaUri = item.requestMetadata.mediaUri?.toString() ?: "",
@@ -425,11 +430,15 @@ open class PlaybackRepository(
425430

426431
private fun createMediaItem(music: MusicModel): MediaItem {
427432
val uniqueId = "${music.sourceId}:${music.id}"
433+
val artistsString = music.artists.joinToString(_artistJoinString.value)
428434
val extras =
429435
android.os.Bundle().apply {
430436
putString("source_id", music.sourceId)
431437
putString("original_id", music.id)
432438
putLong("duration", music.duration)
439+
// 保存原始 title 和 artists,防止歌词 metadata 污染保存/恢复
440+
putString("original_title", music.title)
441+
putString("original_artists", artistsString)
433442
}
434443

435444
val isLocal = music.sourceId == localMusicSource.sourceId
@@ -454,7 +463,7 @@ open class PlaybackRepository(
454463
MediaMetadata
455464
.Builder()
456465
.setTitle(music.title)
457-
.setArtist(music.artists.joinToString(_artistJoinString.value))
466+
.setArtist(artistsString)
458467
.setAlbumTitle(music.album)
459468
.setArtworkUri(android.net.Uri.parse(music.coverUrl))
460469
.setExtras(extras)
@@ -487,4 +496,27 @@ open class PlaybackRepository(
487496
fun release() {
488497
MediaController.releaseFuture(controllerFuture)
489498
}
499+
500+
/**
501+
* 更新当前播放项的媒体通知 metadata(用于显示歌词)
502+
* @param title 新的 title(通常是歌词原文)
503+
* @param artist 新的 artist(通常是翻译或歌曲信息)
504+
*
505+
* 注意:此方法只更新通知覆盖状态,不再使用 replaceMediaItem/seekTo 等会影响播放状态的 API
506+
*/
507+
fun updateCurrentMediaNotificationMetadata(
508+
title: String,
509+
artist: String,
510+
) {
511+
MediaNotificationLyricsState.update(title, artist)
512+
}
513+
514+
/**
515+
* 还原当前播放项的媒体通知 metadata 为原始信息
516+
*
517+
* 注意:此方法只清除通知覆盖状态,不再使用 replaceMediaItem/seekTo 等会影响播放状态的 API
518+
*/
519+
fun restoreCurrentMediaNotificationMetadata() {
520+
MediaNotificationLyricsState.clear()
521+
}
490522
}

0 commit comments

Comments
 (0)