From 79b93a74ceab3d24c22328c1c73e632fe2640d00 Mon Sep 17 00:00:00 2001 From: nutine <40691418+nutine@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:20:42 +0400 Subject: [PATCH] feat(subtitles): implement subtitle translation proxy support --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 82 +++++++++++++++++++ .../ui/settings/SettingsPlayer.kt | 25 ++++++ .../cloudstream3/utils/DataStoreHelper.kt | 11 +++ .../drawable/ic_baseline_auto_awesome_24.xml | 10 +++ app/src/main/res/values/strings.xml | 11 +++ app/src/main/res/xml/settings_player.xml | 5 ++ 6 files changed, 144 insertions(+) create mode 100644 app/src/main/res/drawable/ic_baseline_auto_awesome_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 4495a560262..af017032096 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -50,6 +50,9 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog +import java.net.URLEncoder import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId @@ -907,6 +910,74 @@ class GeneratorPlayer : FullScreenPlayer() { ) } + private fun handleTranslateSubtitlesClick(ctx: Context, sourceDialog: Dialog) { + val proxyUrl = DataStoreHelper.subtitleTranslationProxy + if (proxyUrl.isNullOrBlank()) { + activity?.showNginxTextInputDialog( + name = ctx.getString(R.string.subtitle_translation_proxy_title), + value = "", + textInputType = null, + dismissCallback = {} + ) { input -> + if (input.isNotBlank()) { + DataStoreHelper.subtitleTranslationProxy = input.trim() + showToast(R.string.subtitle_translation_proxy_saved) + handleTranslateSubtitlesClick(ctx, sourceDialog) + } + } + return + } + + val activeSub = player.getCurrentPreferredSubtitle() + ?: viewModel.state.subtitles.firstOrNull { it.origin == SubtitleOrigin.URL } + + if (activeSub == null) { + showToast(R.string.no_subtitle_selected_to_translate) + return + } + if (activeSub.origin != SubtitleOrigin.URL) { + showToast(R.string.only_url_subtitles_can_be_translated) + return + } + + val languages = listOf( + Pair("🇦🇿 Azerbaijani", "az"), + Pair("🇹🇷 Turkish", "tr"), + Pair("🇬🇧 English", "en"), + Pair("🇷🇺 Russian", "ru"), + Pair("🇩🇪 German", "de"), + Pair("🇪🇸 Spanish", "es"), + Pair("🇫🇷 French", "fr"), + Pair("🇮🇹 Italian", "it"), + Pair("🇸🇦 Arabic", "ar"), + Pair("🇵🇹 Portuguese", "pt") + ) + + activity?.showBottomDialogInstant( + items = languages.map { it.first }, + name = ctx.getString(R.string.select_target_language), + dismissCallback = {} + ) { selectedIndex -> + if (selectedIndex in languages.indices) { + val (label, code) = languages[selectedIndex] + val encodedUrl = URLEncoder.encode(activeSub.getFixedUrl(), "UTF-8") + val proxiedUrl = "${proxyUrl.trimEnd('/')}/translate?url=$encodedUrl&target=$code" + + val translatedData = SubtitleData( + originalName = "$label (AI)", + nameSuffix = "", + url = proxiedUrl, + origin = SubtitleOrigin.URL, + mimeType = MimeTypes.TEXT_VTT, + headers = emptyMap(), + languageCode = code + ) + sourceDialog.dismissSafe(activity) + addAndSelectSubtitles(translatedData) + } + } + } + // Open file picker private val subsPathPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> @@ -1042,6 +1113,17 @@ class GeneratorPlayer : FullScreenPlayer() { } subtitleList.addFooterView(loadFromFileFooter) + val translateSubsFooter: TextView = + layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView + translateSubsFooter.text = ctx.getString(R.string.translate_subtitles) + translateSubsFooter.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_baseline_auto_awesome_24, 0, 0, 0 + ) + translateSubsFooter.setOnClickListener { + handleTranslateSubtitlesClick(ctx, sourceDialog) + } + subtitleList.addFooterView(translateSubsFooter) + var shouldDismiss = true binding.subtitleSettingsBtt.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 2949281153f..5cff7972b25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -24,9 +24,11 @@ import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard class SettingsPlayer : BasePreferenceFragmentCompat() { @@ -247,6 +249,29 @@ class SettingsPlayer : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + val translationProxyPref = getPref(R.string.subtitle_translation_proxy_key) + fun updateProxySummary() { + val current = DataStoreHelper.subtitleTranslationProxy + translationProxyPref?.summary = if (current.isNullOrBlank()) { + getString(R.string.subtitle_translation_proxy_not_set) + } else { + current + } + } + updateProxySummary() + translationProxyPref?.setOnPreferenceClickListener { + activity?.showNginxTextInputDialog( + name = getString(R.string.subtitle_translation_proxy_title), + value = DataStoreHelper.subtitleTranslationProxy ?: "", + textInputType = null, + dismissCallback = {} + ) { input -> + DataStoreHelper.subtitleTranslationProxy = input.trim().ifBlank { null } + updateProxySummary() + } + return@setOnPreferenceClickListener true + } + getPref(R.string.player_source_priority_key)?.setOnPreferenceClickListener { ioSafe { val defaultSources = QualityProfileDialog.getAllDefaultSources() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 340266f6c52..fe8eb6c8f82 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -58,6 +58,7 @@ const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" const val KEY_RESULT_SORT = "result_sort" const val USER_PINNED_PROVIDERS = "user_pinned_providers" // Key for pinned user set +const val SUBTITLE_TRANSLATION_PROXY = "subtitle_translation_proxy" class UserPreferenceDelegate( private val key: String, @@ -160,6 +161,16 @@ object DataStoreHelper { _resultsSortingMode = value.ordinal } + var subtitleTranslationProxy: String? + get() = getKey(SUBTITLE_TRANSLATION_PROXY) + set(value) { + if (value.isNullOrBlank()) { + removeKey(SUBTITLE_TRANSLATION_PROXY) + } else { + setKey(SUBTITLE_TRANSLATION_PROXY, value) + } + } + @Serializable data class Account( @JsonProperty("keyIndex") @SerialName("keyIndex") val keyIndex: Int, diff --git a/app/src/main/res/drawable/ic_baseline_auto_awesome_24.xml b/app/src/main/res/drawable/ic_baseline_auto_awesome_24.xml new file mode 100644 index 00000000000..534ae6bcb10 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_auto_awesome_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d530b59f6e..6e97d33a900 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,6 +435,17 @@ Sync subs 1000 ms Subtitle delay + subtitle_translation_proxy_key + Subtitle Translation Proxy + Proxy server URL for translating subtitles + Not configured. Tap to set proxy URL. + Enter your subtitle translation proxy server URL (e.g. https://your-proxy.com). + Translation proxy URL saved + Translation proxy URL cleared + Translate subtitles + Select target language + Please select a subtitle to translate first + Only online URL subtitles can be translated Use this if the subtitles are shown %d ms too early Use this if subtitles are shown %d ms too late No subtitle delay diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 6e136747448..5394d659691 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -53,6 +53,11 @@ android:key="@string/subtitle_settings_chromecast_key" android:title="@string/chromecast_subtitles_settings" app:summary="@string/chromecast_subtitles_settings_des" /> +