From db36bf5abe9030915cbca579519bd1b8ef8c5ac2 Mon Sep 17 00:00:00 2001 From: Jolimunium Date: Thu, 30 Jul 2026 03:08:19 +0700 Subject: [PATCH] feat(player): add in-player Jimaku API key setup --- .../tachiyomi/ui/player/PlayerViewModel.kt | 12 ++- .../ui/player/controls/PlayerControls.kt | 1 + .../ui/player/controls/PlayerSheets.kt | 2 + .../components/sheets/SubtitleTracksSheet.kt | 84 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) 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 830de880c6..2baca575eb 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 @@ -587,6 +587,7 @@ class PlayerViewModel @JvmOverloads internal constructor( sealed interface JimakuState { data object Idle : JimakuState + data object ApiKeyRequired : JimakuState data class Searching(val title: String) : JimakuState data class EntryResults(val title: String, val entries: List) : JimakuState data class LoadingFiles(val entry: JimakuEntry) : JimakuState @@ -690,6 +691,15 @@ class PlayerViewModel @JvmOverloads internal constructor( subtitlePreferences.jimakuTitleForAnime(currentAnime.value?.id).set(title.trim()) } + fun saveJimakuApiKeyAndSearch(apiKey: String) { + val trimmedApiKey = apiKey.trim() + if (trimmedApiKey.isBlank()) return + + subtitlePreferences.jimakuApiKey().set(trimmedApiKey) + _jimakuState.update { JimakuState.Idle } + searchJimakuSubtitles() + } + fun getCurrentJimakuTitle(): String { return guessCurrentJimakuMedia().title } @@ -699,7 +709,7 @@ class PlayerViewModel @JvmOverloads internal constructor( val apiKey = subtitlePreferences.jimakuApiKey().get().trim() if (apiKey.isBlank()) { - activity.toast("Add your Jimaku API key in player subtitle settings first") + _jimakuState.update { JimakuState.ApiKeyRequired } return } 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 f04d72cb25..135439d03f 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 @@ -766,6 +766,7 @@ fun PlayerControls( onAddSubtitle = viewModel::addSubtitle, onSelectSubtitle = viewModel::selectSub, onSearchJimaku = viewModel::searchJimakuSubtitles, + onSaveJimakuApiKeyAndSearch = viewModel::saveJimakuApiKeyAndSearch, onSelectJimakuEntry = viewModel::loadJimakuFiles, onSelectJimakuFile = viewModel::downloadJimakuSubtitle, onDismissJimaku = viewModel::dismissJimakuDialog, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt index 583fa7f035..592befa539 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt @@ -55,6 +55,7 @@ fun PlayerSheets( onAddSubtitle: (Uri) -> Unit, onSelectSubtitle: (Int) -> Unit, onSearchJimaku: () -> Unit, + onSaveJimakuApiKeyAndSearch: (String) -> Unit, onSelectJimakuEntry: (JimakuEntry) -> Unit, onSelectJimakuFile: (JimakuFile) -> Unit, onDismissJimaku: () -> Unit, @@ -125,6 +126,7 @@ fun PlayerSheets( onSelect = onSelectSubtitle, onAddSubtitle = { subtitlesPicker.launch(arrayOf("*/*")) }, onSearchJimaku = onSearchJimaku, + onSaveJimakuApiKeyAndSearch = onSaveJimakuApiKeyAndSearch, onSelectJimakuEntry = onSelectJimakuEntry, onSelectJimakuFile = onSelectJimakuFile, onDismissJimaku = onDismissJimaku, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt index 8d8c225d84..64f7cd4880 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt @@ -28,17 +28,21 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreTime import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Subtitles +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -52,9 +56,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.ui.player.PlayerViewModel.JimakuState import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack @@ -65,6 +73,8 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +private const val JIMAKU_ACCOUNT_URL = "https://jimaku.cc/account" + @Composable fun SubtitlesSheet( tracks: ImmutableList, @@ -75,6 +85,7 @@ fun SubtitlesSheet( onSelect: (Int) -> Unit, onAddSubtitle: () -> Unit, onSearchJimaku: () -> Unit, + onSaveJimakuApiKeyAndSearch: (String) -> Unit, onSelectJimakuEntry: (JimakuEntry) -> Unit, onSelectJimakuFile: (JimakuFile) -> Unit, onDismissJimaku: () -> Unit, @@ -176,6 +187,7 @@ fun SubtitlesSheet( JimakuDialog( state = jimakuState, onDismissRequest = onDismissJimaku, + onSaveJimakuApiKeyAndSearch = onSaveJimakuApiKeyAndSearch, onSelectEntry = onSelectJimakuEntry, onSelectFile = onSelectJimakuFile, ) @@ -289,15 +301,87 @@ private fun JimakuTitleDialog( ) } +@Composable +private fun JimakuApiKeyDialog( + onDismissRequest: () -> Unit, + onConfirm: (String) -> Unit, +) { + val uriHandler = LocalUriHandler.current + var apiKey by remember { mutableStateOf(TextFieldValue()) } + var hideApiKey by remember { mutableStateOf(true) } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text("Add Jimaku API key") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { + Text( + text = "Paste your API key to continue searching. It will also be saved in player subtitle settings.", + ) + TextButton(onClick = { uriHandler.openUri(JIMAKU_ACCOUNT_URL) }) { + Text("Get an API key at jimaku.cc/account") + } + OutlinedTextField( + value = apiKey, + onValueChange = { apiKey = it }, + label = { Text("Jimaku API key") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + trailingIcon = { + IconButton(onClick = { hideApiKey = !hideApiKey }) { + Icon( + imageVector = if (hideApiKey) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, + contentDescription = if (hideApiKey) { + "Show API key" + } else { + "Hide API key" + }, + ) + } + }, + visualTransformation = if (hideApiKey) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + ) + } + }, + confirmButton = { + TextButton( + enabled = apiKey.text.isNotBlank(), + onClick = { onConfirm(apiKey.text) }, + ) { + Text("Save and search") + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(MR.strings.action_cancel)) + } + }, + ) +} + @Composable private fun JimakuDialog( state: JimakuState, onDismissRequest: () -> Unit, + onSaveJimakuApiKeyAndSearch: (String) -> Unit, onSelectEntry: (JimakuEntry) -> Unit, onSelectFile: (JimakuFile) -> Unit, ) { when (state) { JimakuState.Idle -> Unit + JimakuState.ApiKeyRequired -> JimakuApiKeyDialog( + onDismissRequest = onDismissRequest, + onConfirm = onSaveJimakuApiKeyAndSearch, + ) is JimakuState.Searching -> JimakuProgressDialog( title = "Jimaku", message = "Searching for ${state.title}",