diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a45d4c125..e35e9f8a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -272,6 +272,8 @@ dependencies { implementation(libs.arthenica.smartexceptions) // seeker seek bar implementation(libs.seeker) + // true type parser + implementation(libs.truetypeparser) } androidComponents { 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 056205bad..49c30debc 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 @@ -13,6 +13,7 @@ import android.media.AudioManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Environment import android.os.Handler import android.os.Looper import android.os.ParcelFileDescriptor @@ -573,6 +574,21 @@ class PlayerActivity : BaseActivity() { MPVLib.setPropertyDouble("sub-delay", subtitlesDelay().get() / 1000.0) } + MPVLib.setPropertyString( + "sub-fonts-dir", + File( + Environment.getExternalStorageDirectory().absolutePath + File.separator + + getString(R.string.app_name), + "fonts", + ).path, + ) + + if (playerPreferences.subtitleFont().get().trim() != "") { + MPVLib.setPropertyString("sub-font", playerPreferences.subtitleFont().get()) + } else { + MPVLib.setPropertyString("sub-font", "Sans Serif") + } + MPVLib.setPropertyString("sub-bold", if (boldSubtitles().get()) "yes" else "no") MPVLib.setPropertyString("sub-italic", if (italicSubtitles().get()) "yes" else "no") MPVLib.setPropertyInt("sub-font-size", subtitleFontSize().get()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt index ee9b9961c..f9eb2e1fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt @@ -76,6 +76,8 @@ class PlayerPreferences( fun overrideSubsASS() = preferenceStore.getBoolean("pref_override_subtitles_ass", false) + fun subtitleFont() = preferenceStore.getString("pref_subtitle_font", "Sans Serif") + fun subtitleFontSize() = preferenceStore.getInt("pref_subtitles_font_size", 55) fun boldSubtitles() = preferenceStore.getBoolean("pref_bold_subtitles", false) fun italicSubtitles() = preferenceStore.getBoolean("pref_italic_subtitles", false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleColorPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleColorPage.kt index 386780913..90c5679dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleColorPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleColorPage.kt @@ -66,6 +66,8 @@ private fun SubtitleColors( val borderColorPref = screenModel.preferences.borderColorSubtitles() val backgroundColorPref = screenModel.preferences.backgroundColorSubtitles() + val font by screenModel.preferences.subtitleFont().collectAsState() + Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { SubtitleColorSelector( label = R.string.player_subtitle_text_color, @@ -89,6 +91,7 @@ private fun SubtitleColors( Column(horizontalAlignment = Alignment.CenterHorizontally) { SubtitlePreview( + font = font, isBold = screenModel.preferences.boldSubtitles().collectAsState().value, isItalic = screenModel.preferences.italicSubtitles().collectAsState().value, textColor = Color(textColorPref.collectAsState().value), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt index 5fadf519b..a2e75965f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleFontPage.kt @@ -1,26 +1,37 @@ package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle +import android.os.Environment import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.SnackbarDefaults.backgroundColor import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.FormatBold import androidx.compose.material.icons.outlined.FormatItalic import androidx.compose.material.icons.outlined.FormatSize +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.yubyf.truetypeparser.TTFFile +import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.OutlinedNumericChooser import eu.kanade.presentation.util.collectAsState import eu.kanade.tachiyomi.R @@ -29,6 +40,7 @@ import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel import `is`.xyz.mpv.MPVLib import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.padding +import java.io.File @Composable fun SubtitleFontPage(screenModel: PlayerSettingsScreenModel) { @@ -41,6 +53,7 @@ fun SubtitleFontPage(screenModel: PlayerSettingsScreenModel) { private fun SubtitleFont( screenModel: PlayerSettingsScreenModel, ) { + val font by screenModel.preferences.subtitleFont().collectAsState() val boldSubtitles by screenModel.preferences.boldSubtitles().collectAsState() val italicSubtitles by screenModel.preferences.italicSubtitles().collectAsState() val subtitleFontSize by screenModel.preferences.subtitleFontSize().collectAsState() @@ -65,6 +78,30 @@ private fun SubtitleFont( screenModel.preferences.subtitleFontSize().set(it) } + val updateFont: (String) -> Unit = { + MPVLib.setPropertyString("sub-font", it) + screenModel.preferences.subtitleFont().set(it) + } + + val context = LocalContext.current + val fontList by remember { + derivedStateOf { + val customFonts = File( + Environment.getExternalStorageDirectory().absolutePath + + File.separator + context.getString(R.string.app_name) + + File.separator, + "fonts", + ).listFiles { file -> + file.extension.equals("ttf", true) || + file.extension.equals("otf", true) + }?.map { + TTFFile.open(it).families.values.toTypedArray()[0] to it.absolutePath + } ?: emptyList() + listOf("Sans Serif" to "") + customFonts + } + } + var selectingFont by remember { mutableStateOf(false) } + Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), @@ -74,11 +111,13 @@ private fun SubtitleFont( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth(), ) { - Icon( - imageVector = Icons.Outlined.FormatSize, - contentDescription = null, - modifier = Modifier.size(32.dp), - ) + IconButton(onClick = { selectingFont = true }) { + Icon( + imageVector = Icons.Outlined.FormatSize, + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + } OutlinedNumericChooser( label = stringResource(id = R.string.player_font_size_text_field), @@ -111,7 +150,26 @@ private fun SubtitleFont( ) } + DropdownMenu(expanded = selectingFont, onDismissRequest = { selectingFont = false }) { + fontList.map { + val fontName = it.first + DropdownMenuItem( + text = { Text(fontName) }, + onClick = { updateFont(fontName) }, + trailingIcon = { + if (font == fontName) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + ) + } + }, + ) + } + } + SubtitlePreview( + font = font, isBold = boldSubtitles, isItalic = italicSubtitles, textColor = Color(textColor), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt index c2f6cb4f8..1424d8adb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/sheets/subtitle/SubtitleSettingsSheet.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle import android.graphics.Rect import android.graphics.Typeface import android.os.Build +import android.os.Environment import androidx.annotation.RequiresApi import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -25,6 +26,7 @@ import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -32,11 +34,13 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.yubyf.truetypeparser.TTFFile import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel import tachiyomi.presentation.core.components.material.padding +import java.io.File @Composable fun SubtitleSettingsSheet( @@ -70,41 +74,38 @@ fun SubtitleSettingsSheet( @Composable fun OutLineText( text: String, + font: Typeface, outlineColor: Color = Color.Black, textColor: Color = Color.White, - backgroundColor: Color = Color.Black, isBold: Boolean = false, isItalic: Boolean = false, + backgroundColor: Color = Color.Black, ) { val textPaintStroke = Paint().asFrameworkPaint().apply { - typeface = Typeface.create( - Typeface.SANS_SERIF, - if (isBold) FontWeight.Bold.weight else FontWeight.Normal.weight, - isItalic, - ) + typeface = font isAntiAlias = true style = android.graphics.Paint.Style.STROKE - textSize = 16f + textSize = 48f color = outlineColor.toArgb() - strokeWidth = 2f - strokeMiter = 2f + strokeWidth = 12f + strokeMiter = 8f strokeJoin = android.graphics.Paint.Join.ROUND // change the text alignment from left to center (basically shift the anchor point of the text) // keep in mind that this only affects horizontal alignment // https://developer.android.com/reference/android/graphics/Paint.Align textAlign = android.graphics.Paint.Align.CENTER + isFakeBoldText = isBold + textSkewX = if (isItalic) -0.25f else 0f } val textPaint = Paint().asFrameworkPaint().apply { - typeface = Typeface.create( - Typeface.SANS_SERIF, - if (isBold) FontWeight.Bold.weight else FontWeight.Normal.weight, - isItalic, - ) + typeface = font isAntiAlias = true style = android.graphics.Paint.Style.FILL - textSize = 16f + textSize = 48f color = textColor.toArgb() textAlign = android.graphics.Paint.Align.CENTER + isFakeBoldText = isBold + textSkewX = if (isItalic) -0.25f else 0f } Canvas(modifier = Modifier.fillMaxSize()) { drawIntoCanvas { @@ -141,15 +142,31 @@ fun OutLineText( } } -@RequiresApi(Build.VERSION_CODES.P) @Composable fun SubtitlePreview( + font: String, isBold: Boolean, isItalic: Boolean, textColor: Color, borderColor: Color, backgroundColor: Color, ) { + val fontMap = File( + Environment.getExternalStorageDirectory().absolutePath + + File.separator + LocalContext.current.getString(R.string.app_name) + + File.separator, + "fonts", + ).listFiles { file -> + file.extension.equals("ttf", true) || + file.extension.equals("otf", true) + }?.associateBy( + { TTFFile.open(it).families.values.toTypedArray()[0] }, + { it.absolutePath }, + ) ?: emptyMap() + + val fontFile = fontMap.keys.firstOrNull { it.contains(font, true) } + ?.let { Typeface.createFromFile(fontMap[it]?.let(::File)) } ?: Typeface.SANS_SERIF + Box( modifier = Modifier .padding(vertical = MaterialTheme.padding.medium) @@ -159,6 +176,7 @@ fun SubtitlePreview( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { OutLineText( text = stringResource(R.string.player_subtitle_settings_example), + font = fontFile, outlineColor = borderColor, textColor = textColor, isBold = isBold, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b658e51a6..85d20422c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,6 +95,8 @@ arthenica-smartexceptions = "com.arthenica:smart-exception-java:0.1.1" seeker = "io.github.2307vivek:seeker:1.1.1" +truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4" + [bundles] reactivex = ["rxandroid", "rxjava"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] diff --git a/i18n/src/main/res/values/strings-aniyomi.xml b/i18n/src/main/res/values/strings-aniyomi.xml index ccce7c9f0..bfafaae85 100644 --- a/i18n/src/main/res/values/strings-aniyomi.xml +++ b/i18n/src/main/res/values/strings-aniyomi.xml @@ -346,6 +346,7 @@ Text Border Background + Font Name Exclude from data saver