diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb8bdee65..c3ac2e9a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - `Other` - for technical stuff. ## [Unreleased] +### Added +- Added system font fallback ([@Secozzi](https://github.com/Secozzi)) ([#156](https://github.com/quickdesh/Animiru/pull/156)) ## [v0.19.7.3] - 2026-04-17 ### Improved diff --git a/app/src/main/java/animiru/feature/mpvfiles/MpvConfig.kt b/app/src/main/java/animiru/feature/mpvfiles/MpvConfig.kt index 2c79b0aed9..e0bba787c2 100644 --- a/app/src/main/java/animiru/feature/mpvfiles/MpvConfig.kt +++ b/app/src/main/java/animiru/feature/mpvfiles/MpvConfig.kt @@ -37,6 +37,7 @@ class MpvConfig( copyUserFiles(mpvDir) copyFontsDirectory(mpvDir) copyAssets(mpvDir) + writeFontsConf(context, mpvDir) } } @@ -116,7 +117,7 @@ class MpvConfig( private fun copyAssets(mpvDir: UniFile) { val assetManager = context.assets - val files = arrayOf("subfont.ttf", "cacert.pem") + val files = arrayOf("cacert.pem") for (filename in files) { var ins: InputStream? = null var out: OutputStream? = null @@ -141,6 +142,43 @@ class MpvConfig( } } + private fun writeFontsConf(context: Context, mpvDir: UniFile) { + val parts = mutableListOf( + "", + // Android system fonts reside here + "/system/fonts/", + "/product/fonts/", + // User provided fonts + "${mpvDir.createDirectory(MPV_FONTS_DIR)!!.filePath!!}", + // Point fontconfig to the right cache path so that caching works + "${context.cacheDir.path}", + // Conveniently there is *no* Java API to query the system default fonts, but we can + // manually specify the font families we know Android uses and provides by default. + // (compare to 60-latin.conf shipped with fontconfig) + "serif", + "Noto Serif", + "", + "Sans Serif", + "", + "Roboto", + "Noto Sans", // other languages + "", + "", + "monospace", + "Droid Sans Mono", + "", + "", + ) + try { + val file = mpvDir.createFile("fonts.conf") + file?.openOutputStream()?.bufferedWriter()?.use { + it.write(parts.joinToString("\n")) + } + } catch (e: IOException) { + logcat(LogPriority.ERROR, e) { "Failed to write fonts.conf" } + } + } + private fun deleteAndGet(parent: UniFile, name: String): UniFile { parent.createDirectory(name)?.delete() return parent.createDirectory(name)!! diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt index 038a14220d..aa37e4ee70 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt @@ -28,6 +28,7 @@ object PlayerSettingsSubtitleScreen : SearchableSettings { val whitelist = subtitlePreferences.subtitleWhitelist val blacklist = subtitlePreferences.subtitleBlacklist val blackBars = subtitlePreferences.subtitleBlackBars + val systemFonts = subtitlePreferences.subtitleSystemFonts return listOf( Preference.PreferenceItem.EditTextInfoPreference( @@ -86,6 +87,10 @@ object PlayerSettingsSubtitleScreen : SearchableSettings { title = stringResource(AMMR.strings.player_pref_subtitle_black_bars), subtitle = stringResource(AMMR.strings.player_pref_subtitle_black_bars_summary), ), + Preference.PreferenceItem.SwitchPreference( + preference = systemFonts, + title = stringResource(AMMR.strings.player_pref_subtitle_system_fonts), + ), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/player/components/ExposedTextDropDownMenu.kt b/app/src/main/java/eu/kanade/presentation/player/components/ExposedTextDropDownMenu.kt index 433d128119..2f82c01db2 100644 --- a/app/src/main/java/eu/kanade/presentation/player/components/ExposedTextDropDownMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/player/components/ExposedTextDropDownMenu.kt @@ -18,6 +18,10 @@ package eu.kanade.presentation.player.components import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -31,6 +35,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList @Composable @@ -64,15 +71,32 @@ fun ExposedTextDropDownMenu( .fillMaxWidth(), ) + val sizeOfOneItem by remember { + mutableStateOf(50.dp) + } + val screenHeight = with(LocalDensity.current) { + LocalWindowInfo.current.containerSize.height.toDp() + } + val height by remember(options.size) { + val itemsSize = sizeOfOneItem * options.size + mutableStateOf(minOf(itemsSize, screenHeight * 3 / 4)) + } + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - options.forEach { option: String -> - DropdownMenuItem( - text = { Text(text = option) }, - onClick = { - expanded = false - onValueChangedEvent(option) - }, - ) + LazyColumn( + modifier = Modifier + .width(500.dp) + .height(height), + ) { + items(options) { option -> + DropdownMenuItem( + text = { Text(text = option) }, + onClick = { + expanded = false + onValueChangedEvent(option) + }, + ) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 387616569a..0decf7371e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -450,7 +450,6 @@ class PlayerActivity : BaseActivity() { private fun setupPlayerMPV() { val mpvDir = UniFile.fromFile(applicationContext.filesDir)!!.createDirectory(MPV_DIR)!! - val fontsDirectory = mpvDir.createDirectory(MPV_FONTS_DIR)!! val mpvConfFile = mpvDir.createFile("mpv.conf")!! advancedPlayerPreferences.mpvConf.get().let { mpvConfFile.writeText(it) } @@ -459,8 +458,6 @@ class PlayerActivity : BaseActivity() { player.init(mpv) - mpv.setPropertyString("sub-fonts-dir", fontsDirectory.filePath!!) - mpv.setPropertyString("osd-fonts-dir", fontsDirectory.filePath!!) val showBlackBars = if (subtitlePreferences.subtitleBlackBars.get()) "yes" else "no" mpv.setOptionString("sub-ass-force-margins", showBlackBars) mpv.setOptionString("sub-use-margins", showBlackBars) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index dfe6e29ef4..c221842c1a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -27,6 +27,7 @@ import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.yubyf.truetypeparser.TTFFile import dev.icerock.moko.resources.StringResource import eu.kanade.domain.anime.interactor.SetAnimeViewerFlags import eu.kanade.domain.base.BasePreferences @@ -67,6 +68,7 @@ import eu.kanade.tachiyomi.ui.player.loader.HosterLoader import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences import eu.kanade.tachiyomi.ui.player.utils.AniSkipApi import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils.Companion.getStringRes @@ -93,6 +95,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn @@ -124,6 +127,7 @@ import tachiyomi.domain.history.interactor.UpsertHistory import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.source.service.SourceManager +import tachiyomi.domain.storage.service.StorageManager import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.source.local.isLocal @@ -133,6 +137,7 @@ import java.io.File import java.io.InputStream import java.util.Date import java.util.concurrent.atomic.AtomicBoolean +import kotlin.collections.first import kotlin.coroutines.cancellation.CancellationException class PlayerViewModel @JvmOverloads constructor( @@ -141,6 +146,7 @@ class PlayerViewModel @JvmOverloads constructor( private val json: Json = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), + private val storageManager: StorageManager = Injekt.get(), private val imageSaver: ImageSaver = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(), @@ -155,6 +161,7 @@ class PlayerViewModel @JvmOverloads constructor( private val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(), private val playerPreferences: PlayerPreferences = Injekt.get(), private val audioPreferences: AudioPreferences = Injekt.get(), + private val subtitlePreferences: SubtitlePreferences = Injekt.get(), private val gesturePreferences: GesturePreferences = Injekt.get(), private val basePreferences: BasePreferences = Injekt.get(), private val getCustomButtons: GetCustomButtons = Injekt.get(), @@ -311,7 +318,16 @@ class PlayerViewModel @JvmOverloads constructor( private val _remainingTime = MutableStateFlow(0) val remainingTime = _remainingTime.asStateFlow() + private val _fontList = MutableStateFlow>(persistentListOf()) + val fontList = _fontList.asStateFlow() + init { + viewModelScope.launchIO { + subtitlePreferences.subtitleSystemFonts.changes().collectLatest { + _fontList.update { _ -> fetchFonts(it).toPersistentList() } + } + } + mpv.propFlow("time-pos") .filterNotNull() .onEach(::onSecondReached) @@ -357,6 +373,49 @@ class PlayerViewModel @JvmOverloads constructor( } } + fun fetchFonts(includeSystemFonts: Boolean): List { + val fontFiles = mutableListOf() + + storageManager.getFontsDirectory()?.listFiles()?.filter { file -> + file.name?.lowercase()?.matches(FONT_EXTENSION_REGEX) == true + }?.mapNotNull { + try { + TTFFile.open(it.openInputStream()).families.values.first() + } catch (_: Exception) { + null + } + }?.let { + fontFiles.addAll(it) + } + + if (!includeSystemFonts) { + return fontFiles.distinct() + } + + val fontDirectories = listOf( + "/system/fonts/", + "/product/fonts/", + ) + + for (directory in fontDirectories) { + val dir = File(directory) + if (dir.exists() && dir.isDirectory) { + val files = dir.listFiles() + files?.filter { file -> + file.isFile && file.name.lowercase().matches(FONT_EXTENSION_REGEX) + }?.forEach { file -> + try { + fontFiles.add( + TTFFile.open(file.inputStream()).families.values.first(), + ) + } catch (_: Exception) { } + } + } + } + + return fontFiles.distinct() + } + private fun setCustomButtons(buttons: List) { _customButtons.update { _ -> buttons.toPersistentList() } buttons.firstOrNull { it.isFavorite }?.let { @@ -2155,6 +2214,8 @@ class PlayerViewModel @JvmOverloads constructor( } } +private val FONT_EXTENSION_REGEX = Regex($$""".*\.[ot]tf$""") + fun CustomButton.execute(mpv: MPV) { mpv.command("script-message", "call_button_$id") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt index d241d7f126..a2fe579a5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt @@ -711,6 +711,7 @@ fun PlayerControls( val isItalic by viewModel.mpv.propFlow("sub-italic").collectAsState() val subJustify by viewModel.mpv.propFlow("sub-justify").collectAsState() val subFont by viewModel.mpv.propFlow("sub-font").collectAsState() + val subFontList by viewModel.fontList.collectAsState() val subFontSize by viewModel.mpv.propFlow("sub-font-size").collectAsState() val subBorderStyle by viewModel.mpv.propFlow("sub-border-style").collectAsState() val subBorderSize by viewModel.mpv.propFlow("sub-outline-size").collectAsState() @@ -741,6 +742,7 @@ fun PlayerControls( subJustify?.let { SubtitleJustification.byValue(it) } ?: subtitlePreferences.subtitleJustification.get(), subFont = subFont ?: subtitlePreferences.subtitleFont.get(), + subFontList = subFontList, subFontSize = subFontSize ?: subtitlePreferences.subtitleFontSize.get(), subBorderStyle = subBorderStyle?.let { SubtitlesBorderStyle.byValue(it) } ?: subtitlePreferences.borderStyleSubtitles.get(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt index 5ff6751895..9c1fad4230 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt @@ -46,6 +46,7 @@ import eu.kanade.tachiyomi.ui.player.controls.components.panels.VideoSettingsPan import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.SubtitleAssOverride import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification +import kotlinx.collections.immutable.ImmutableList import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -58,6 +59,7 @@ fun PlayerPanels( isItalic: Boolean, subJustify: SubtitleJustification, subFont: String, + subFontList: ImmutableList, subFontSize: Int, subBorderStyle: SubtitlesBorderStyle, subBorderSize: Int, @@ -129,6 +131,7 @@ fun PlayerPanels( isItalic = isItalic, justify = subJustify, font = subFont, + fontList = subFontList, fontSize = subFontSize, borderStyle = subBorderStyle, borderSize = subBorderSize, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt index 6f0c466557..a01de7625a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import eu.kanade.tachiyomi.ui.player.controls.components.panels.components.MultiCardPanel import eu.kanade.tachiyomi.ui.player.settings.SubtitleAssOverride import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification +import kotlinx.collections.immutable.ImmutableList import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.i18n.stringResource @@ -33,6 +34,7 @@ fun SubtitleSettingsPanel( isItalic: Boolean, justify: SubtitleJustification, font: String, + fontList: ImmutableList, fontSize: Int, borderStyle: SubtitlesBorderStyle, borderSize: Int, @@ -74,6 +76,7 @@ fun SubtitleSettingsPanel( isItalic = isItalic, justify = justify, font = font, + fontList = fontList, fontSize = fontSize, borderStyle = borderStyle, borderSize = borderSize, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt index 5aa4f9a499..34e349d3b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt @@ -46,7 +46,6 @@ import androidx.compose.material3.MaterialTheme 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 @@ -55,7 +54,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.yubyf.truetypeparser.TTFFile import dev.icerock.moko.resources.StringResource import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.player.components.ExpandableCard @@ -67,17 +65,12 @@ import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences import `is`.xyz.mpv.MPV -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.collections.immutable.ImmutableList import tachiyomi.core.common.preference.deleteAndGet -import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get @Composable fun SubtitleSettingsTypographyCard( @@ -85,6 +78,7 @@ fun SubtitleSettingsTypographyCard( isItalic: Boolean, justify: SubtitleJustification, font: String, + fontList: ImmutableList, fontSize: Int, borderStyle: SubtitlesBorderStyle, borderSize: Int, @@ -100,36 +94,14 @@ fun SubtitleSettingsTypographyCard( onReset: () -> Unit, modifier: Modifier = Modifier, ) { - val preferences = remember { Injekt.get() } - val storageManager = remember { Injekt.get() } var isExpanded by remember { mutableStateOf(true) } - val fontsDir = storageManager.getFontsDirectory() - val fonts by remember { mutableStateOf(mutableListOf(preferences.subtitleFont.defaultValue())) } var fontsLoadingIndicator: (@Composable () -> Unit)? by remember { val indicator: (@Composable () -> Unit) = { CircularProgressIndicator(Modifier.size(32.dp)) } mutableStateOf(indicator) } - LaunchedEffect(Unit) { - if (fontsDir == null) { - fontsLoadingIndicator = null - return@LaunchedEffect - } - withContext(Dispatchers.IO) { - fontsDir.listFiles()?.filter { file -> - file.name?.lowercase()?.matches(FONT_EXTENSION_REGEX) == true - }?.mapNotNull { - runCatching { TTFFile.open(it.openInputStream()).families.values.first() }.getOrNull() - }?.let { - fonts.addAll( - it.distinct(), - ) - } - fontsLoadingIndicator = null - } - } ExpandableCard( isExpanded = isExpanded, @@ -204,10 +176,10 @@ fun SubtitleSettingsTypographyCard( ) ExposedTextDropDownMenu( selectedValue = font, - options = fonts.toImmutableList(), + options = fontList, label = stringResource(AYMR.strings.player_sheets_sub_typography_font), onValueChangedEvent = onFontChange, - leadingIcon = fontsLoadingIndicator, + leadingIcon = if (fontList.isEmpty()) fontsLoadingIndicator else null, ) } SliderItem( @@ -287,8 +259,6 @@ fun SubtitleSettingsTypographyCard( } } -private val FONT_EXTENSION_REGEX = Regex($$""".*\.[ot]tf$""") - fun resetTypography( mpv: MPV, preferences: SubtitlePreferences, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt index d3d9511f5e..4cfe30f7c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt @@ -22,6 +22,7 @@ class SubtitlePreferences( val subtitleWhitelist: Preference = preferenceStore.getString("pref_subtitle_whitelist", "") val subtitleBlacklist: Preference = preferenceStore.getString("pref_subtitle_blacklist", "") val subtitleBlackBars: Preference = preferenceStore.getBoolean("pref_subtitle_black_bars", false) + val subtitleSystemFonts: Preference = preferenceStore.getBoolean("pref_subtitle_system_fonts", false) // Non-preferences diff --git a/gradle/aniyomi.versions.toml b/gradle/aniyomi.versions.toml index 9758a36446..de6f0f26ac 100644 --- a/gradle/aniyomi.versions.toml +++ b/gradle/aniyomi.versions.toml @@ -1,5 +1,5 @@ [versions] -mpv-lib = "0.1.13" +mpv-lib = "0.1.14" arthenica-smartexceptions = "0.2.1" constraint-layout = "1.1.0" ffmpeg-kit = "1.18" diff --git a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml index 9f6735b7aa..c5afaf652c 100644 --- a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml @@ -208,6 +208,7 @@ Render on black bars Display subtitles on black bars when available + Include system fonts Video settings