diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e77bf4794..f21b73b70 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,5 @@ @file:Suppress("UnstableApiUsage") -val isFullBuild: Boolean by rootProject.extra plugins { id("com.android.application") @@ -19,8 +18,8 @@ android { applicationId = "com.dd3boh.outertune" minSdk = 24 targetSdk = 35 - versionCode = 34 - versionName = "0.7.0" + versionCode = 35 + versionName = "0.7.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -33,6 +32,13 @@ android { debug { applicationIdSuffix = ".debug" } + + // userdebug is release builds without minify + create("userdebug") { + initWith(getByName("release")) + isMinifyEnabled = false + isShrinkResources = false + } } buildFeatures { @@ -77,13 +83,16 @@ android { abiFilters.add("x86_64") } } - // for uncommon, but non-obscure architectures - create("uncommon_abi") { - dimension = "abi" - ndk { - abiFilters.addAll(listOf("x86", "x86_64", "armeabi-v7a")) + } + + applicationVariants.all { + val variant = this + variant.outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + .forEach { output -> + val outputFileName = "OuterTune-${variant.versionName}-${variant.baseName}.apk" + output.outputFileName = outputFileName } - } } compileOptions { diff --git a/app/src/main/java/com/dd3boh/outertune/MainActivity.kt b/app/src/main/java/com/dd3boh/outertune/MainActivity.kt index 8f168e2c3..e62bba11f 100644 --- a/app/src/main/java/com/dd3boh/outertune/MainActivity.kt +++ b/app/src/main/java/com/dd3boh/outertune/MainActivity.kt @@ -245,6 +245,7 @@ class MainActivity : ComponentActivity() { lateinit var syncUtils: SyncUtils lateinit var activityLauncher: ActivityLauncherHelper + lateinit var connectivityObserver: NetworkConnectivityObserver private var playerConnection by mutableStateOf(null) private val serviceConnection = object : ServiceConnection { @@ -314,11 +315,11 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) activityLauncher = ActivityLauncherHelper(this) + connectivityObserver = NetworkConnectivityObserver(this) setContent { val haptic = LocalHapticFeedback.current - val connectivityObserver = NetworkConnectivityObserver(this) val isNetworkConnected by connectivityObserver.networkStatus.collectAsState(false) val enableDynamicTheme by rememberPreference(DynamicThemeKey, defaultValue = true) diff --git a/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt b/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt index 9e8447e4b..46b6696f4 100644 --- a/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt @@ -65,6 +65,8 @@ val ShowLyricsKey = booleanPreferencesKey("showLyrics") val LyricsTextPositionKey = stringPreferencesKey("lyricsTextPosition") val MultilineLrcKey = booleanPreferencesKey("multilineLrc") val LyricTrimKey = booleanPreferencesKey("lyricTrim") +val LyricSourcePrefKey = booleanPreferencesKey("preferLocalLyrics") +val LyricFontSizeKey = intPreferencesKey("lyricFontSize") /** @@ -77,6 +79,7 @@ val MaxImageCacheSizeKey = intPreferencesKey("maxImageCacheSize") * Privacy */ val PauseListenHistoryKey = booleanPreferencesKey("pauseListenHistory") +val PauseRemoteListenHistoryKey = booleanPreferencesKey("pauseRemoteListenHistory") val PauseSearchHistoryKey = booleanPreferencesKey("pauseSearchHistory") val EnableKugouKey = booleanPreferencesKey("enableKugou") val EnableLrcLibKey = booleanPreferencesKey("enableLrcLib") diff --git a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt index 1c970ddd8..52182a331 100644 --- a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt +++ b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt @@ -4,16 +4,18 @@ import android.content.Context import android.os.Build import android.util.LruCache import androidx.annotation.RequiresApi +import com.dd3boh.outertune.constants.LyricSourcePrefKey import com.dd3boh.outertune.db.MusicDatabase +import com.dd3boh.outertune.db.entities.LyricsEntity import com.dd3boh.outertune.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.dd3boh.outertune.models.MediaMetadata +import com.dd3boh.outertune.utils.dataStore +import com.dd3boh.outertune.utils.get import com.dd3boh.outertune.utils.reportException import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first import javax.inject.Inject -// true will prioritize local lyric files over all cloud providers, true is vice versa -private const val PREFER_LOCAL_LYRIC = true class LyricsHelper @Inject constructor( @ApplicationContext private val context: Context, ) { @@ -23,17 +25,25 @@ class LyricsHelper @Inject constructor( /** * Retrieve lyrics from all sources * + * How lyrics are resolved are determined by PreferLocalLyrics settings key. If this is true, prioritize local lyric + * files over all cloud providers, true is vice versa. + * + * Lyrics stored in the database are fetched first. If this is not available, it is resolved by other means. + * If local lyrics are preferred, lyrics from the lrc file is fetched, and then resolve by other means. + * * @param mediaMetadata Song to fetch lyrics for * @param database MusicDatabase connection. Database lyrics are prioritized over all sources. * If no database is provided, the database source is disabled */ suspend fun getLyrics(mediaMetadata: MediaMetadata, database: MusicDatabase? = null): String { + val prefLocal = context.dataStore.get(LyricSourcePrefKey, true) + val cached = cache.get(mediaMetadata.id)?.firstOrNull() if (cached != null) { return cached.lyrics } val dbLyrics = database?.lyrics(mediaMetadata.id)?.let { it.first()?.lyrics } - if (dbLyrics != null) { + if (dbLyrics != null && !prefLocal) { return dbLyrics } @@ -46,19 +56,38 @@ class LyricsHelper @Inject constructor( val remoteLyrics: String? // fallback to secondary provider when primary is unavailable - if (PREFER_LOCAL_LYRIC) { + if (prefLocal) { if (localLyrics != null) { return localLyrics } + if (dbLyrics != null) { + return dbLyrics + } // "lazy eval" the remote lyrics cuz it is laughably slow remoteLyrics = getRemoteLyrics(mediaMetadata) if (remoteLyrics != null) { + database?.query { + upsert( + LyricsEntity( + id = mediaMetadata.id, + lyrics = remoteLyrics + ) + ) + } return remoteLyrics } } else { remoteLyrics = getRemoteLyrics(mediaMetadata) if (remoteLyrics != null) { + database?.query { + upsert( + LyricsEntity( + id = mediaMetadata.id, + lyrics = remoteLyrics + ) + ) + } return remoteLyrics } else if (localLyrics != null) { return localLyrics @@ -66,6 +95,14 @@ class LyricsHelper @Inject constructor( } + database?.query { + upsert( + LyricsEntity( + id = mediaMetadata.id, + lyrics = LYRICS_NOT_FOUND + ) + ) + } return LYRICS_NOT_FOUND } diff --git a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsUtils.kt b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsUtils.kt index 8f1b49589..305fb8a44 100644 --- a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsUtils.kt +++ b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsUtils.kt @@ -81,28 +81,6 @@ object LyricsUtils { return emptyList() + parseLyrics(lyrics, parserOptions).convertForLegacy() } - - // (OuterTune) Support compressed lyrics. We don't support word for word lyrics (Extended LRC) anyways - fun uncompressLyrics(lyrics: String): String { - var result = "" - - lyrics.lines().forEach { it -> - val regex = Regex("""\[\d+:\d+\.\d+\]""") - val matches = regex.findAll(it) - - if (matches.count() < 1) { - result += "$it\n" - } else { - val c = it.split("\\[(\\d+):(\\d{2})([.:]\\d+)?]".toRegex()) - val content = c[c.size - 1] - for (string in matches) { - result += "${string.value}${content}\n" - } - } - } - return result - } - @OptIn(UnstableApi::class) fun loadAndParseLyricsFile(musicFile: File?, parserOptions: LrcParserOptions): SemanticLyrics? { val lrcFile = musicFile?.let { File(it.parentFile, it.nameWithoutExtension + ".lrc") } diff --git a/app/src/main/java/com/dd3boh/outertune/lyrics/SemanticLyrics.kt b/app/src/main/java/com/dd3boh/outertune/lyrics/SemanticLyrics.kt index d586415f6..352e5f663 100644 --- a/app/src/main/java/com/dd3boh/outertune/lyrics/SemanticLyrics.kt +++ b/app/src/main/java/com/dd3boh/outertune/lyrics/SemanticLyrics.kt @@ -81,6 +81,7 @@ private sealed class SyntacticLrc { companion object { // also eats space if present val timeMarksRegex = "\\[(\\d{2}):(\\d{2})([.:]\\d+)?]".toRegex() + val timeMarksAfterWsRegex = "([ \t]+)\\[(\\d{2}):(\\d{2})([.:]\\d+)?]".toRegex() val timeWordMarksRegex = "<(\\d{2}):(\\d{2})([.:]\\d+)?>".toRegex() val metadataRegex = "\\[([a-zA-Z#]+):([^]]*)]".toRegex() @@ -133,16 +134,21 @@ private sealed class SyntacticLrc { // but hey, we tried. Can't do much about it. // If you want to write something that looks like a timestamp into your lyrics, // you'll probably have to delete the following three lines. - if (!(out.isNotEmpty() && out.last() is NewLine - || out.isNotEmpty() && out.last() is SyncPoint) - ) + if (!(out.lastOrNull() is NewLine || out.lastOrNull() is SyncPoint)) out.add(NewLine.SyntheticNewLine()) out.add(SyncPoint(parseTime(tmMatch))) pos += tmMatch.value.length continue } + // Skip spaces in between of compressed lyric sync points. They really are + // completely useless information we can and should discard. + val tmwMatch = timeMarksAfterWsRegex.matchAt(text, pos) + if (out.lastOrNull() is SyncPoint && pos + 7 < text.length && tmwMatch != null) { + pos += tmwMatch.groupValues[1].length + continue + } // Speaker points can only appear directly after a sync point - if (out.isNotEmpty() && out.last() is SyncPoint) { + if (out.lastOrNull() is SyncPoint) { if (pos + 2 < text.length && text.regionMatches(pos, "v1:", 0, 3)) { out.add(SpeakerTag(SpeakerEntity.Voice1)) pos += 3 @@ -240,7 +246,7 @@ private sealed class SyntacticLrc { .let { if (it == pos - 1) text.length else it } .let { if (it == pos) it + 1 else it } val subText = text.substring(pos, firstUnsafeCharPos) - val last = if (out.isNotEmpty()) out.last() else null + val last = out.lastOrNull() // Only count lyric text as lyric text if there is at least one kind of timestamp // associated. if (out.indexOfLast { it is NewLine } < @@ -259,7 +265,7 @@ private sealed class SyntacticLrc { } pos = firstUnsafeCharPos } - if (out.isNotEmpty() && out.last() is SyncPoint) + if (out.lastOrNull() is SyncPoint) out.add(InvalidText("")) if (out.isNotEmpty() && out.last() !is NewLine) out.add(NewLine.SyntheticNewLine()) diff --git a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt index e343dd38f..ac201a52e 100644 --- a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt +++ b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt @@ -63,6 +63,7 @@ import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleLike import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleRepeatMode import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleShuffle import com.dd3boh.outertune.constants.PauseListenHistoryKey +import com.dd3boh.outertune.constants.PauseRemoteListenHistoryKey import com.dd3boh.outertune.constants.PersistentQueueKey import com.dd3boh.outertune.constants.PlayerVolumeKey import com.dd3boh.outertune.constants.RepeatModeKey @@ -73,7 +74,6 @@ import com.dd3boh.outertune.constants.minPlaybackDurKey import com.dd3boh.outertune.db.MusicDatabase import com.dd3boh.outertune.db.entities.Event import com.dd3boh.outertune.db.entities.FormatEntity -import com.dd3boh.outertune.db.entities.LyricsEntity import com.dd3boh.outertune.db.entities.RelatedSongMap import com.dd3boh.outertune.di.DownloadCache import com.dd3boh.outertune.extensions.SilentHandler @@ -351,18 +351,6 @@ class MusicService : MediaLibraryService(), dataStore.data.map { it[ShowLyricsKey] ?: false }.distinctUntilChanged() ) { mediaMetadata, showLyrics -> mediaMetadata to showLyrics - }.collectLatest(scope) { (mediaMetadata, showLyrics) -> - if (showLyrics && mediaMetadata != null && database.lyrics(mediaMetadata.id).first() == null) { - val lyrics = lyricsHelper.getLyrics(mediaMetadata) - database.query { - upsert( - LyricsEntity( - id = mediaMetadata.id, - lyrics = lyrics - ) - ) - } - } } dataStore.data @@ -832,7 +820,7 @@ class MusicService : MediaLibraryService(), } // TODO: support playlist id - if (mediaItem.metadata?.isLocal != true) { + if (mediaItem.metadata?.isLocal != true && dataStore.get(PauseRemoteListenHistoryKey, true)) { CoroutineScope(Dispatchers.IO).launch { val playbackUrl = database.format(mediaItem.mediaId).first()?.playbackUrl ?: YTPlayerUtils.playerResponseForMetadata(mediaItem.mediaId, null).getOrNull()?.playbackTracking?.videostatsPlaybackUrl?.baseUrl diff --git a/app/src/main/java/com/dd3boh/outertune/ui/component/CreatePlaylistDialog.kt b/app/src/main/java/com/dd3boh/outertune/ui/component/CreatePlaylistDialog.kt index 8c40c681a..6192535e6 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/component/CreatePlaylistDialog.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/component/CreatePlaylistDialog.kt @@ -73,12 +73,12 @@ fun CreatePlaylistDialog( ) { Column() { Text( - text = "Sync Playlist", + text = stringResource(R.string.create_sync_playlist), style = MaterialTheme.typography.titleLarge, ) Text( - text = "Note: This allows for syncing with YouTube Music. This is NOT changeable later. You cannot add local songs to synced playlists.", + text = stringResource(R.string.create_sync_playlist_description), style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxWidth(0.7f) ) diff --git a/app/src/main/java/com/dd3boh/outertune/ui/component/Dialog.kt b/app/src/main/java/com/dd3boh/outertune/ui/component/Dialog.kt index fc2a370ab..5e5a0418a 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/component/Dialog.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/component/Dialog.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Minimize import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog @@ -35,6 +37,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -42,8 +45,10 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -317,6 +322,139 @@ fun ActionPromptDialog( } ) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CounterDialog( + title: String, + description: String? = null, + initialValue: Int, + upperBound: Int = 100, + lowerBound: Int = 0, + unitDisplay: String = "", + onDismiss: () -> Unit, + onConfirm: (Int) -> Unit, + onReset: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, +) = BasicAlertDialog( + onDismissRequest = { onDismiss() }, + content = { + val tempValue = rememberSaveable { + mutableIntStateOf(initialValue) + } + Column( + modifier = Modifier + .background( + MaterialTheme.colorScheme.background, + RoundedCornerShape(DialogCornerRadius) + ) + .fillMaxWidth(0.8f) + .padding(16.dp) + ) { + Column(modifier = Modifier.padding(12.dp)) { + // title and description + Text( + text = title, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.titleLarge, + ) + if (description != null) { + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } + + // plus minus buttons + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + ) { + IconButton( + onClick = { + if (tempValue.intValue < upperBound) { + tempValue.value += 1 + } + }, + onLongClick = {} + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + } + + + IconButton( + onClick = { + if (tempValue.intValue > lowerBound) { + tempValue.value -= 1 + } + }, + onLongClick = {} + ) { + Icon( + imageVector = Icons.Rounded.Minimize, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + } + } + + // slider and value display + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "${tempValue.intValue}$unitDisplay", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Slider( + value = tempValue.intValue.toFloat(), + onValueChange = { tempValue.intValue = it.toInt() }, + valueRange = lowerBound.toFloat()..upperBound.toFloat() + ) + } + + // bottom options + // always have an ok, but explicit cancel/reset is optional + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + if (onReset != null) + Row(modifier = Modifier.weight(1f)) { + TextButton( + onClick = { onReset() }, + ) { + Text(stringResource(R.string.reset)) + } + } + + TextButton( + onClick = { onConfirm(tempValue.intValue) } + ) { + Text(stringResource(android.R.string.ok)) + } + + if (onCancel != null) + TextButton( + onClick = { onCancel() } + ) { + Text(stringResource(android.R.string.cancel)) + } + } + } + } + } +) + @Composable fun DetailsDialog( mediaMetadata: MediaMetadata, diff --git a/app/src/main/java/com/dd3boh/outertune/ui/component/Lyrics.kt b/app/src/main/java/com/dd3boh/outertune/ui/component/Lyrics.kt index 0d6ee0e25..2108b0fd6 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/component/Lyrics.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/component/Lyrics.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.sp import com.dd3boh.outertune.LocalPlayerConnection import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.DarkModeKey +import com.dd3boh.outertune.constants.LyricFontSizeKey import com.dd3boh.outertune.constants.LyricTrimKey import com.dd3boh.outertune.constants.LyricsTextPositionKey import com.dd3boh.outertune.constants.MultilineLrcKey @@ -92,6 +93,7 @@ fun Lyrics( val landscapeOffset = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val lyricsTextPosition by rememberEnumPreference(LyricsTextPositionKey, LyricsPosition.CENTER) + val lyricsFontSize by rememberPreference(LyricFontSizeKey, 20) val mediaMetadata by playerConnection.mediaMetadata.collectAsState() val lyricsEntity by playerConnection.currentLyrics.collectAsState(initial = null) @@ -110,8 +112,7 @@ fun Lyrics( val lines: List = remember(lyrics) { if (lyrics == null || lyrics == LYRICS_NOT_FOUND) emptyList() else if (lyrics.startsWith("[")) listOf(HEAD_LYRICS_ENTRY) + - loadAndParseLyricsString(LyricsUtils.uncompressLyrics(lyrics), - LyricsUtils.LrcParserOptions(lyricTrim.value, multilineLrc.value, "Unable to parse lyrics")) + loadAndParseLyricsString(lyrics, LyricsUtils.LrcParserOptions(lyricTrim.value, multilineLrc.value, "Unable to parse lyrics")) else lyrics.lines().mapIndexed { index, line -> LyricsEntry(index * 100L, line) } } val isSynced = remember(lyrics) { @@ -254,7 +255,7 @@ fun Lyrics( ) { index, item -> Text( text = item.content, - fontSize = 20.sp, + fontSize = lyricsFontSize.sp, color = textColor, textAlign = when (lyricsTextPosition) { LyricsPosition.LEFT -> TextAlign.Left diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/LyricsMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/LyricsMenu.kt index a1ef7bca0..3aa2aeb41 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/LyricsMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/LyricsMenu.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.ExpandLess import androidx.compose.material.icons.rounded.ExpandMore @@ -158,7 +159,12 @@ fun LyricsMenu( TextButton( onClick = { - viewModel.search(searchMediaMetadata.id, titleField.text, artistField.text, searchMediaMetadata.duration) + viewModel.search( + searchMediaMetadata.id, + titleField.text, + artistField.text, + searchMediaMetadata.duration + ) showSearchResultDialog = true } ) { @@ -284,6 +290,47 @@ fun LyricsMenu( } } + var showDeleteLyric by remember { + mutableStateOf(false) + } + + if (showDeleteLyric) { + DefaultDialog( + onDismiss = { showDeleteLyric = false }, + content = { + Text( + text = stringResource(R.string.delete_lyric_confirm, mediaMetadataProvider().title), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 18.dp) + ) + }, + buttons = { + TextButton( + onClick = { + showDeleteLyric = false + } + ) { + Text(text = stringResource(android.R.string.cancel)) + } + + TextButton( + onClick = { + showDeleteLyric = false + onDismiss() + + lyricsProvider()?.let { + database.query { + delete(it) + } + } + } + ) { + Text(text = stringResource(android.R.string.ok)) + } + } + ) + } + GridMenu( contentPadding = PaddingValues( start = 8.dp, @@ -311,5 +358,14 @@ fun LyricsMenu( ) { showSearchDialog = true } + if (lyricsProvider() != null) { + // TODO: hide this for when lrc exists and lyrics is not in the database + GridMenuItem( + icon = Icons.Rounded.Delete, + title = R.string.delete, + ) { + showDeleteLyric = true + } + } } } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/SetupWizard.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/SetupWizard.kt index 016e81ab3..15d8dd8ad 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/SetupWizard.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/SetupWizard.kt @@ -197,7 +197,7 @@ fun SetupWizard( } ) { Text( - text = "Back", + text = stringResource(R.string.action_back), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) @@ -235,7 +235,7 @@ fun SetupWizard( } ) { Text( - text = "Next", + text = stringResource(R.string.action_next), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) @@ -305,7 +305,7 @@ fun SetupWizard( ) Column(verticalArrangement = Arrangement.Center) { Text( - text = "Welcome to OuterTune", + text = stringResource(R.string.oobe_welcome_message), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, modifier = Modifier @@ -329,7 +329,7 @@ fun SetupWizard( tint = MaterialTheme.colorScheme.secondary ) Text( - text = "YouTube Music at your fingertips", + text = stringResource(R.string.oobe_ytm_content_description), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, modifier = Modifier @@ -347,7 +347,7 @@ fun SetupWizard( contentDescription = null ) Text( - text = "AD free playback", + text = stringResource(R.string.oobe_ad_free_description), style = MaterialTheme.typography.titleSmall, modifier = Modifier .padding(horizontal = 8.dp, vertical = 8.dp) @@ -365,7 +365,7 @@ fun SetupWizard( tint = MaterialTheme.colorScheme.tertiary ) Text( - text = "Account sync", + text = stringResource(R.string.oobe_ytm_sync_description), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, modifier = Modifier @@ -383,7 +383,7 @@ fun SetupWizard( tint = MaterialTheme.colorScheme.onSurface, ) Text( - text = "Local music playback", + text = stringResource(R.string.oobe_local_playback_description), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, modifier = Modifier @@ -406,7 +406,7 @@ fun SetupWizard( } ) { Text( - text = "I have a backup", + text = stringResource(R.string.oobe_use_backup), style = MaterialTheme.typography.bodyMedium, ) } @@ -418,7 +418,7 @@ fun SetupWizard( } ) { Text( - text = "Skip", + text = stringResource(R.string.action_skip), style = MaterialTheme.typography.bodyMedium, ) } @@ -451,7 +451,7 @@ fun SetupWizard( } Text( - text = "Interface", + text = stringResource(R.string.grp_interface), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, modifier = Modifier @@ -679,7 +679,7 @@ fun SetupWizard( } Text( - text = "Account", + text = stringResource(R.string.account), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, modifier = Modifier @@ -764,7 +764,7 @@ fun SetupWizard( // local media 3 -> { Text( - text = "Local media", + text = stringResource(R.string.local_player_settings_title), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, modifier = Modifier @@ -793,7 +793,7 @@ fun SetupWizard( PreferenceEntry( - title = { Text("Click here to scan for songs") }, + title = { Text(stringResource(R.string.oobe_local_scan_tooltip)) }, icon = { Icon(Icons.Rounded.Backup, null) }, onClick = { navController.navigate("settings/local") @@ -807,7 +807,7 @@ fun SetupWizard( Column(verticalArrangement = Arrangement.Center) { Text( - text = "All done", + text = stringResource(R.string.oobe_complete), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, modifier = Modifier @@ -828,7 +828,7 @@ fun SetupWizard( verticalAlignment = Alignment.Top, ) { Text( - text = "Information", + text = stringResource(R.string.info), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) @@ -902,7 +902,7 @@ private fun SortHeaderDummy( modifier = modifier.padding(vertical = 8.dp) ) { Text( - text = "Name", + text = stringResource(R.string.sort_by_name), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelLarge, modifier = Modifier diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AboutScreen.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AboutScreen.kt index ad616ae3c..b41004ed7 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AboutScreen.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AboutScreen.kt @@ -54,6 +54,8 @@ fun AboutScreen( ) { val uriHandler = LocalUriHandler.current + val showDebugInfo = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "userdebug" + Column( modifier = Modifier .fillMaxWidth() @@ -93,11 +95,11 @@ fun AboutScreen( Spacer(Modifier.width(4.dp)) - if (BuildConfig.DEBUG) { + if (showDebugInfo) { Spacer(Modifier.width(4.dp)) Text( - text = "DEBUG", + text = BuildConfig.BUILD_TYPE.uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary, modifier = Modifier @@ -141,7 +143,7 @@ fun AboutScreen( verticalAlignment = Alignment.Top, ) { Text( - text = "Special Thanks", + text = stringResource(R.string.special_thanks), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) @@ -149,13 +151,13 @@ fun AboutScreen( } Text( - text = "Zion Huang for InnerTune", + text = stringResource(R.string.attrib_zhuang), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary ) // debug info - if (BuildConfig.DEBUG) { + if (showDebugInfo) { Spacer(Modifier.height(400.dp)) Row( verticalAlignment = Alignment.Top, diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AppearanceSettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AppearanceSettings.kt index e1b90ff2f..9b9ae5a94 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AppearanceSettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/AppearanceSettings.kt @@ -146,32 +146,6 @@ fun AppearanceSettings( } mutableTabs.move(from.index, to.index) } - LaunchedEffect(reorderableState.isAnyItemDragging) { - if (!reorderableState.isAnyItemDragging) { - dragInfo?.let { (from, to) -> -// if (from == to) { -// return@LaunchedEffect -// } - -// mutableTabs.apply { -// clear() -// -// val enabled = decodeTabString(enabledTabs) -// addAll(enabled) -// add(NavigationTab.NULL) -// addAll(NavigationTab.entries.filter { item -> enabled.none { it == item || item == NavigationTab.NULL } }) -// } - } - } - } - - - -// val reorderableState = rememberReorderableLazyListState( -// onMove = { from, to -> -// mutableTabs.move(from.index, to.index) -// } -// ) fun updateTabs() { mutableTabs.apply { @@ -194,9 +168,8 @@ fun AppearanceSettings( .verticalScroll(rememberScrollState()) ) { PreferenceGroupTitle( - title = "Theme" + title = stringResource(R.string.theme) ) - SwitchPreference( title = { Text(stringResource(R.string.enable_dynamic_theme)) }, icon = { Icon(Icons.Rounded.Palette, null) }, @@ -238,23 +211,20 @@ fun AppearanceSettings( ) PreferenceGroupTitle( - title = "Layout" + title = stringResource(R.string.grp_interface) ) - SwitchPreference( title = { Text(stringResource(R.string.new_interface)) }, icon = { Icon(Icons.Rounded.Palette, null) }, checked = newInterfaceStyle, onCheckedChange = onNewInterfaceStyleChange ) - SwitchPreference( title = { Text(stringResource(R.string.show_liked_and_downloaded_playlist)) }, icon = { Icon(Icons.AutoMirrored.Rounded.PlaylistPlay, null) }, checked = showLikedAndDownloadedPlaylist, onCheckedChange = onShowLikedAndDownloadedPlaylistChange ) - SwitchPreference( title = { Text(stringResource(R.string.slim_navbar_title)) }, description = stringResource(R.string.slim_navbar_description), @@ -262,9 +232,8 @@ fun AppearanceSettings( checked = slimNav, onCheckedChange = onSlimNavChange ) - PreferenceEntry( - title = { Text("Tab arrangement") }, + title = { Text(stringResource(R.string.tab_arrangement)) }, icon = { Icon(Icons.Rounded.Reorder, null) }, onClick = { showTabArrangement = true @@ -273,7 +242,7 @@ fun AppearanceSettings( if (showTabArrangement) ActionPromptDialog( - title = "Arrange tabs", + title = stringResource(R.string.tab_arrangement), onDismiss = { showTabArrangement = false }, onConfirm = { var encoded = encodeTabString(mutableTabs) diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/BackupAndRestore.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/BackupAndRestore.kt index a11c822db..0ab188fa9 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/BackupAndRestore.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/BackupAndRestore.kt @@ -145,7 +145,7 @@ fun BackupAndRestore( ) { item { Text( - text = "Could not import:", + text = stringResource(R.string.m3u_import_song_failed), fontSize = 24.sp, fontWeight = FontWeight.Bold, maxLines = 1, diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/ContentSettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/ContentSettings.kt index 3c039a5c1..59b0f4950 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/ContentSettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/ContentSettings.kt @@ -1,6 +1,7 @@ package com.dd3boh.outertune.ui.screens.settings import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState @@ -107,7 +108,7 @@ fun ContentSettings( .verticalScroll(rememberScrollState()) ) { PreferenceGroupTitle( - title = "ACCOUNT" + title = stringResource(R.string.account) ) PreferenceEntry( title = { Text(if (isLoggedIn) accountName else stringResource(R.string.login)) }, @@ -202,7 +203,7 @@ fun ContentSettings( ) PreferenceGroupTitle( - title = "LOCALIZATION" + title = stringResource(R.string.localization) ) ListPreference( title = { Text(stringResource(R.string.content_language)) }, @@ -230,16 +231,14 @@ fun ContentSettings( ) PreferenceGroupTitle( - title = "PROXY" + title = stringResource(R.string.proxy) ) - SwitchPreference( title = { Text(stringResource(R.string.enable_proxy)) }, checked = proxyEnabled, onCheckedChange = onProxyEnabledChange ) - - if (proxyEnabled) { + AnimatedVisibility(proxyEnabled) { ListPreference( title = { Text(stringResource(R.string.proxy_type)) }, selectedValue = proxyType, diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/LyricsSettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/LyricsSettings.kt index f05c6780e..7b1adc4d8 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/LyricsSettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/LyricsSettings.kt @@ -10,12 +10,17 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.rounded.ContentCut import androidx.compose.material.icons.rounded.Lyrics +import androidx.compose.material.icons.rounded.TextFields import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarScrollBehavior 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.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavController @@ -23,11 +28,16 @@ import com.dd3boh.outertune.LocalPlayerAwareWindowInsets import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.EnableKugouKey import com.dd3boh.outertune.constants.EnableLrcLibKey +import com.dd3boh.outertune.constants.LyricFontSizeKey +import com.dd3boh.outertune.constants.LyricSourcePrefKey import com.dd3boh.outertune.constants.LyricTrimKey import com.dd3boh.outertune.constants.LyricsTextPositionKey import com.dd3boh.outertune.constants.MultilineLrcKey +import com.dd3boh.outertune.ui.component.CounterDialog import com.dd3boh.outertune.ui.component.EnumListPreference import com.dd3boh.outertune.ui.component.IconButton +import com.dd3boh.outertune.ui.component.PreferenceEntry +import com.dd3boh.outertune.ui.component.PreferenceGroupTitle import com.dd3boh.outertune.ui.component.SwitchPreference import com.dd3boh.outertune.ui.utils.backToMain import com.dd3boh.outertune.utils.rememberEnumPreference @@ -46,43 +56,64 @@ fun LyricsSettings( val (lyricsPosition, onLyricsPositionChange) = rememberEnumPreference(LyricsTextPositionKey, defaultValue = LyricsPosition.CENTER) val (multilineLrc, onMultilineLrcChange) = rememberPreference(MultilineLrcKey, defaultValue = true) val (lyricTrim, onLyricTrimChange) = rememberPreference(LyricTrimKey, defaultValue = false) + val (lyricFontSize, onLyricFontSizeChange) = rememberPreference(LyricFontSizeKey, defaultValue = 20) + val (preferLocalLyric, onPreferLocalLyric) = rememberPreference(LyricSourcePrefKey, defaultValue = true) + var showFontSizeDialog by remember { + mutableStateOf(false) + } + + // lyrics font size + if (showFontSizeDialog) { + CounterDialog( + title = stringResource(R.string.lyrics_font_Size), + initialValue = lyricFontSize, + upperBound = 32, + lowerBound = 8, + unitDisplay = " pt", + onDismiss = { showFontSizeDialog = false }, + onConfirm = { + onLyricFontSizeChange(it) + showFontSizeDialog = false + }, + onReset = { onLyricFontSizeChange(20) }, + onCancel = { showFontSizeDialog = false } + ) + } + Column( Modifier .windowInsetsPadding(LocalPlayerAwareWindowInsets.current) .verticalScroll(rememberScrollState()) ) { - // providers + PreferenceGroupTitle( + title = "Lyric sources" + ) SwitchPreference( title = { Text(stringResource(R.string.enable_lrclib)) }, icon = { Icon(Icons.Rounded.Lyrics, null) }, checked = enableLrcLib, onCheckedChange = onEnableLrcLibChange ) - SwitchPreference( title = { Text(stringResource(R.string.enable_kugou)) }, icon = { Icon(Icons.Rounded.Lyrics, null) }, checked = enableKugou, onCheckedChange = onEnableKugouChange ) - - // lyrics position - EnumListPreference( - title = { Text(stringResource(R.string.lyrics_text_position)) }, - icon = { Icon(Icons.Rounded.Lyrics, null) }, - selectedValue = lyricsPosition, - onValueSelected = onLyricsPositionChange, - valueText = { - when (it) { - LyricsPosition.LEFT -> stringResource(R.string.left) - LyricsPosition.CENTER -> stringResource(R.string.center) - LyricsPosition.RIGHT -> stringResource(R.string.right) - } - } + // prioritize local lyric files over all cloud providers + SwitchPreference( + title = { Text(stringResource(R.string.lyrics_prefer_local)) }, + description = stringResource(R.string.lyrics_prefer_local_description), + icon = { Icon(Icons.Rounded.ContentCut, null) }, + checked = preferLocalLyric, + onCheckedChange = onPreferLocalLyric ) + PreferenceGroupTitle( + title = "Parser" + ) // multiline lyrics SwitchPreference( title = { Text(stringResource(R.string.lyrics_multiline_title)) }, @@ -100,6 +131,29 @@ fun LyricsSettings( onCheckedChange = onLyricTrimChange ) + PreferenceGroupTitle( + title = "Formatting" + ) + // lyrics position + EnumListPreference( + title = { Text(stringResource(R.string.lyrics_text_position)) }, + icon = { Icon(Icons.Rounded.Lyrics, null) }, + selectedValue = lyricsPosition, + onValueSelected = onLyricsPositionChange, + valueText = { + when (it) { + LyricsPosition.LEFT -> stringResource(R.string.left) + LyricsPosition.CENTER -> stringResource(R.string.center) + LyricsPosition.RIGHT -> stringResource(R.string.right) + } + } + ) + PreferenceEntry( + title = { Text( stringResource(R.string.lyrics_font_Size)) }, + description = "$lyricFontSize sp", + icon = { Icon(Icons.Rounded.TextFields, null) }, + onClick = { showFontSizeDialog = true } + ) } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt index dc0acb16c..8b66faa8f 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt @@ -7,9 +7,6 @@ import android.content.pm.PackageManager import android.os.Build import android.widget.Toast import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -26,32 +23,27 @@ import androidx.compose.material.icons.rounded.NoCell import androidx.compose.material.icons.rounded.Sync import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf 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.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.navigation.NavController import com.dd3boh.outertune.LocalPlayerAwareWindowInsets import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.AudioNormalizationKey +import com.dd3boh.outertune.constants.AudioOffload import com.dd3boh.outertune.constants.AudioQuality import com.dd3boh.outertune.constants.AudioQualityKey -import com.dd3boh.outertune.constants.AudioOffload import com.dd3boh.outertune.constants.KeepAliveKey import com.dd3boh.outertune.constants.PersistentQueueKey import com.dd3boh.outertune.constants.SkipOnErrorKey @@ -59,7 +51,7 @@ import com.dd3boh.outertune.constants.SkipSilenceKey import com.dd3boh.outertune.constants.StopMusicOnTaskClearKey import com.dd3boh.outertune.constants.minPlaybackDurKey import com.dd3boh.outertune.playback.KeepAlive -import com.dd3boh.outertune.ui.component.ActionPromptDialog +import com.dd3boh.outertune.ui.component.CounterDialog import com.dd3boh.outertune.ui.component.EnumListPreference import com.dd3boh.outertune.ui.component.IconButton import com.dd3boh.outertune.ui.component.PreferenceEntry @@ -91,9 +83,6 @@ fun PlayerSettings( var showMinPlaybackDur by remember { mutableStateOf(false) } - var tempminPlaybackDur by remember { - mutableIntStateOf(minPlaybackDur) - } fun toggleKeepAlive(newValue: Boolean) { // disable and request if disabled @@ -142,39 +131,22 @@ fun PlayerSettings( if (showMinPlaybackDur) { - ActionPromptDialog( - title = "Minimum playback duration", + CounterDialog( + title = stringResource(R.string.min_playback_duration), + description = stringResource(R.string.min_playback_duration_description), + initialValue = minPlaybackDur, + upperBound = 100, + lowerBound = 0, + unitDisplay = "%", onDismiss = { showMinPlaybackDur = false }, onConfirm = { showMinPlaybackDur = false - onMinPlaybackDurChange(tempminPlaybackDur) + onMinPlaybackDurChange(it) }, onCancel = { showMinPlaybackDur = false - tempminPlaybackDur = minPlaybackDur - } - ) { - Text( - text = "The minimum amount of a song that must be played before it is considered \"played\"", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(horizontal = 4.dp) - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "${tempminPlaybackDur}%", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(horizontal = 8.dp) - ) - Slider( - value = tempminPlaybackDur.toFloat(), - onValueChange = { tempminPlaybackDur = it.toInt() }, - valueRange = 0f..100f - ) } - } + ) } Column( @@ -183,7 +155,7 @@ fun PlayerSettings( .verticalScroll(rememberScrollState()) ) { PreferenceGroupTitle( - title = "Interface" + title = stringResource(R.string.grp_layout) ) SwitchPreference( title = { Text(stringResource(R.string.persistent_queue)) }, @@ -199,13 +171,13 @@ fun PlayerSettings( onClick = { navController.navigate("settings/player/lyrics") } ) PreferenceEntry( - title = { Text("Minimum playback duration") }, + title = { Text(stringResource(R.string.min_playback_duration)) }, icon = { Icon(Icons.Rounded.Sync, null) }, onClick = { showMinPlaybackDur = true } ) PreferenceGroupTitle( - title = "Audio" + title = stringResource(R.string.audio) ) EnumListPreference( title = { Text(stringResource(R.string.audio_quality)) }, @@ -241,7 +213,7 @@ fun PlayerSettings( ) PreferenceGroupTitle( - title = "Advanced" + title = stringResource(R.string.prefs_advanced) ) SwitchPreference( title = { Text(stringResource(R.string.stop_music_on_task_clear)) }, diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PrivacySettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PrivacySettings.kt index ecf70622b..5511ecf8b 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PrivacySettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PrivacySettings.kt @@ -31,6 +31,7 @@ import com.dd3boh.outertune.LocalDatabase import com.dd3boh.outertune.LocalPlayerAwareWindowInsets import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.PauseListenHistoryKey +import com.dd3boh.outertune.constants.PauseRemoteListenHistoryKey import com.dd3boh.outertune.constants.PauseSearchHistoryKey import com.dd3boh.outertune.constants.UseLoginForBrowse import com.dd3boh.outertune.ui.component.DefaultDialog @@ -50,6 +51,7 @@ fun PrivacySettings( ) { val database = LocalDatabase.current val (pauseListenHistory, onPauseListenHistoryChange) = rememberPreference(key = PauseListenHistoryKey, defaultValue = false) + val (pauseRemoteListenHistory, onPauseRemoteListenHistoryChange) = rememberPreference(key = PauseRemoteListenHistoryKey, defaultValue = true) val (pauseSearchHistory, onPauseSearchHistoryChange) = rememberPreference(key = PauseSearchHistoryKey, defaultValue = false) val (useLoginForBrowse, onUseLoginForBrowseChange) = rememberPreference(key = UseLoginForBrowse, defaultValue = false) @@ -134,6 +136,13 @@ fun PrivacySettings( checked = pauseListenHistory, onCheckedChange = onPauseListenHistoryChange ) + SwitchPreference( + title = { Text(stringResource(R.string.pause_listen_history)) }, + icon = { Icon(Icons.Rounded.History, null) }, + checked = pauseRemoteListenHistory, + onCheckedChange = onPauseRemoteListenHistoryChange, + isEnabled = pauseListenHistory + ) PreferenceEntry( title = { Text(stringResource(R.string.clear_listen_history)) }, icon = { Icon(Icons.Rounded.ClearAll, null) }, @@ -154,7 +163,6 @@ fun PrivacySettings( PreferenceGroupTitle( title = stringResource(R.string.account) ) - SwitchPreference( title = { Text(stringResource(R.string.use_login_for_browse)) }, description = stringResource(R.string.use_login_for_browse_desc), diff --git a/app/src/main/java/com/dd3boh/outertune/viewmodels/BackupRestoreViewModel.kt b/app/src/main/java/com/dd3boh/outertune/viewmodels/BackupRestoreViewModel.kt index ebd853b9b..0b954e0a1 100644 --- a/app/src/main/java/com/dd3boh/outertune/viewmodels/BackupRestoreViewModel.kt +++ b/app/src/main/java/com/dd3boh/outertune/viewmodels/BackupRestoreViewModel.kt @@ -149,7 +149,7 @@ class BackupRestoreViewModel @Inject constructor( } }.onFailure { reportException(it) - Toast.makeText(context, R.string.m3u_import_failed, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.m3u_import_playlist_failed, Toast.LENGTH_SHORT).show() } if (songs.isEmpty()) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da25d4b19..ec8af2165 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,7 @@ Selection mode Like all + Do you really want to delete the stored lyrics for \"%s\"? Date modified @@ -361,42 +362,60 @@ Date liked + Sync Playlist + Note: This allows for syncing with YouTube Music. This is NOT changeable later. You cannot add local songs to synced playlists. Play count - Debug + Interface + Layout Logout + Advanced + Debug + + + Zion Huang for InnerTune + Special thanks + - Player background style + New interface layout + Blur Follow theme Gradient - Blur - New interface layout + Player background style Show liked and downloaded playlists in library"> Slim navbar Remove text labels to reduce navbar height + Arrange tabs - + Automatically download your liked songs + Localization + This is an ADVANCED login method. As an alternative to the web portal, you may directly enter or update your login token here. For example, this can speed up logging in on multiple devices. Please note that any invalid token formats the app fails to parse will not be accepted Tap to show token Tap again to copy or edit - This is an ADVANCED login method. As an alternative to the web portal, you may directly enter or update your login token here. For example, this can speed up logging in on multiple devices. Please note that any invalid token formats the app fails to parse will not be accepted Automatically sync with your YouTube Music account - Automatically download your liked songs Wifi only + Audio Enable offload Use the offload audio path for audio playback. Disabling this may increase power usage but can be useful if you experience issues with audio playback or post processing Prevent system killing app Use a foreground notification to prevent OuterTune from being randomly closed by the system. + Minimum playback duration + The minimum amount of a song that must be played before it is considered \"played\" + Proxy Lyrics Read multiline lyrics Treat all the lines between sync points as one lyric text Remove spaces around lyrics + Prefer local lyric files + Prioritize resolving local lyric files before using all cloud providers. Turn ths off to prioritize cloud providers + Font size Enable local media @@ -407,7 +426,7 @@ Restoring InnerTune backups are supported, albeit a possibility of anomalies due to technical limitations Import a playlist (m3u) - Failed to import playlist + Failed to import playlist Local scanner @@ -436,12 +455,33 @@ Use integrated TagLib metadata extractor Use FFMpeg metadata extractor. Requires external package to be installed - Rescan the entire library and reload all songs\' metadata Try to link local files\' artists with ones on YouTube Music + + Share listen history with YouTube Music + + + Could not import: + Experimental Enable developer settings Reveals additional advanced settings intended for development use + + + Welcome to OuterTune + Click here to scan for songs + Setup complete + YouTube Music at your fingertips + AD free playback + Account sync + Local music playback + I have a backup + + + Back + Next + Skip + Information diff --git a/build.gradle.kts b/build.gradle.kts index 624c8990a..ec59e8965 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,10 +4,6 @@ plugins { } buildscript { - val isFullBuild by extra { - gradle.startParameter.taskNames.none { task -> task.contains("foss", ignoreCase = true) } - } - repositories { google() mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97180e9a4..000a31bbd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -androidGradlePlugin = "8.7.3" +androidGradlePlugin = "8.8.0" annotation = "1.9.1" kotlin = "2.0.21" compose = "1.7.6" diff --git a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt index e33b2ea76..84dc5e7a5 100644 --- a/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt +++ b/kugou/src/main/java/com/zionhuang/kugou/KuGou.kt @@ -55,7 +55,13 @@ object KuGou { val keyword = generateKeyword(title, artist) getLyricsCandidate(keyword, duration)?.let { candidate -> downloadLyrics(candidate.id, candidate.accesskey).content.decodeBase64String() - .normalize() + .normalize().let { + if (it == "[00:01.58]纯音乐,请欣赏") { + // instrumental tracks should report as having no lyrics + throw IllegalStateException("No lyrics candidate") + } + it + } } ?: throw IllegalStateException("No lyrics candidate") }